@phnx-labs/agents-cli 1.22.77 → 1.22.78

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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # Changelog
2
2
 
3
- ## 1.22.77
3
+ ## 1.22.78
4
+
5
+ - **The launch env carries the human's Phoenix ID profile picture.** A resolved human actor now gets `AGENTS_ACTOR_AVATAR` (https URL) when the Phoenix ID session signed in on the device belongs to the same email, so tools an agent runs — `artifacts render`, shares — can show the person instead of initials. A shared box signed in as someone else lends nothing to whoever SSH-ed in, and a child spawn inherits the value like the rest of `AGENTS_ACTOR*`. `agents auth whoami` now also refreshes the persisted picture from `/api/v1/auth/me`, which Phoenix ID returns as of phnx-labs/phoenix-id#7. Source: `cli/src/lib/actor.ts`, `cli/src/commands/auth.ts`.
4
6
 
5
7
  - **`agents repo pull` / `agents sync` no longer wedge on a CLI-regenerated `~/.agents/agents.yaml` (PHNX-3968).** The shared central config was rewritten by config commands without being committed, so between the daemon's publish ticks the tree sat dirty on `agents.yaml`; an incoming peer commit that also touched it made the pull refuse with *"incoming changes touch uncommitted paths: agents.yaml"*, forcing manual autostash gymnastics on the fleet. Central config edits are now committed synchronously by the command that makes them (after the meta lock releases, skipped for the daemon whose own publish tick owns its commits), so the tree is clean at rest and the trip cannot occur. A genuinely dirty-and-differing central file now *refuses* the pull (data-safe, self-heals on the next commit) rather than silently taking the remote copy. The user-repo `CHANGELOG.md` (a duplicate of the shipped one) is de-tracked via `.git/info/exclude` so it stops churning the tree. Source: `cli/src/lib/state.ts`, `cli/src/lib/git.ts`, `cli/src/lib/browser/registry.ts`, `cli/src/lib/project-resources.ts`.
6
8
 
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { PhoenixApiError, clearSession, createSpace, createSpaceInvite, fetchWhoAmI, listSpaceMembers, listSpaces, pollDeviceToken, readSession, removeSpaceMember, resolveMemberFromList, resolveSpaceFromList, slugify, startDeviceAuthorization, updateSpaceMemberRole, writeSession, } from '../lib/identity/index.js';
2
+ import { PhoenixApiError, clearSession, createSpace, createSpaceInvite, fetchWhoAmI, listSpaceMembers, listSpaces, pollDeviceToken, readSession, refreshSessionAvatar, removeSpaceMember, resolveMemberFromList, resolveSpaceFromList, slugify, startDeviceAuthorization, updateSpaceMemberRole, writeSession, } from '../lib/identity/index.js';
3
3
  import { setHelpSections } from '../lib/help.js';
4
4
  import { runOrDie } from '../lib/format.js';
5
5
  import { registerMintCommand } from './auth-mint.js';
@@ -60,6 +60,9 @@ async function whoami(json) {
60
60
  }
61
61
  try {
62
62
  const me = await fetchWhoAmI();
63
+ // Keep the persisted profile picture current: the actor env and share
64
+ // attribution read it from the session file, never from the network.
65
+ await refreshSessionAvatar(me);
63
66
  if (json) {
64
67
  console.log(JSON.stringify({ signedIn: true, ...me }, null, 2));
65
68
  return;
@@ -1,3 +1,4 @@
1
+ import { type PhoenixSession } from './identity/client.js';
1
2
  import type { ActorConfig } from './types.js';
2
3
  export type ActorKind = 'human' | 'agent';
3
4
  export interface ResolvedActor {
@@ -20,6 +21,13 @@ export interface ResolvedActor {
20
21
  * happens to be signed into tailscale with.
21
22
  */
22
23
  phoenixId?: string;
24
+ /**
25
+ * The human's hosted profile picture (https URL) from the Phoenix ID session
26
+ * signed in on this device, attached only when that session belongs to the
27
+ * same email as the resolved actor. Rides the env as `AGENTS_ACTOR_AVATAR` so
28
+ * tools the agent runs (artifacts, shares) can show the person, not initials.
29
+ */
30
+ avatarUrl?: string;
23
31
  }
24
32
  /** Result of `tailscale whois --json <ip>` we care about. */
25
33
  export interface WhoisIdentity {
@@ -34,6 +42,13 @@ export interface WhoisIdentity {
34
42
  * from the tailnet DisplayName + login email.
35
43
  */
36
44
  export declare function actorFromIdentity(who: WhoisIdentity | undefined, host: string, actors: Record<string, ActorConfig>): ResolvedActor;
45
+ /**
46
+ * The Phoenix ID profile picture for a resolved human, when the session signed
47
+ * in on this device is that same person (matched on email, case-insensitive).
48
+ * A shared box signed in as someone else must not lend its picture to whoever
49
+ * SSH-ed in, so a mismatch yields nothing. Pure: the session is injected.
50
+ */
51
+ export declare function actorAvatar(actor: ResolvedActor, session: PhoenixSession | null): string | undefined;
37
52
  /**
38
53
  * Injectable tailscale resolvers, so tests can drive the SSH-whois and
39
54
  * local-self branches deterministically without a real tailscale on the box
@@ -43,6 +58,8 @@ export declare function actorFromIdentity(who: WhoisIdentity | undefined, host:
43
58
  export interface ActorResolvers {
44
59
  whois: (ip: string) => WhoisIdentity | undefined;
45
60
  self: () => WhoisIdentity | undefined;
61
+ /** The Phoenix ID session on this device (a local file read, no network). */
62
+ session: () => PhoenixSession | null;
46
63
  }
47
64
  /**
48
65
  * Compute the actor for a given environment. The only impurity is the tailscale
package/dist/lib/actor.js CHANGED
@@ -25,6 +25,7 @@
25
25
  import { spawnSync } from 'child_process';
26
26
  import { machineId } from './machine-id.js';
27
27
  import { parseSshConnection } from './session/provenance.js';
28
+ import { readSession } from './identity/client.js';
28
29
  import { readMeta } from './state.js';
29
30
  /** Hard cap on the whois shell-out — it runs on the spawn hot path. */
30
31
  const WHOIS_TIMEOUT_MS = 2000;
@@ -145,9 +146,28 @@ function inheritedActor(env) {
145
146
  email: env.AGENTS_ACTOR_EMAIL || undefined,
146
147
  github: env.AGENTS_ACTOR_GITHUB || undefined,
147
148
  phoenixId: env.AGENTS_ACTOR_PHOENIX_ID || undefined,
149
+ avatarUrl: httpsUrl(env.AGENTS_ACTOR_AVATAR),
148
150
  };
149
151
  }
150
- const defaultResolvers = { whois: tailscaleWhois, self: tailscaleSelf };
152
+ /** An avatar is only ever an https URL; anything else is not an avatar. */
153
+ function httpsUrl(value) {
154
+ const trimmed = value?.trim();
155
+ return trimmed && /^https:\/\/\S+$/i.test(trimmed) ? trimmed : undefined;
156
+ }
157
+ /**
158
+ * The Phoenix ID profile picture for a resolved human, when the session signed
159
+ * in on this device is that same person (matched on email, case-insensitive).
160
+ * A shared box signed in as someone else must not lend its picture to whoever
161
+ * SSH-ed in, so a mismatch yields nothing. Pure: the session is injected.
162
+ */
163
+ export function actorAvatar(actor, session) {
164
+ if (actor.kind !== 'human' || !actor.email || !session?.email)
165
+ return undefined;
166
+ if (actor.email.trim().toLowerCase() !== session.email.trim().toLowerCase())
167
+ return undefined;
168
+ return httpsUrl(session.avatarUrl);
169
+ }
170
+ const defaultResolvers = { whois: tailscaleWhois, self: tailscaleSelf, session: readSession };
151
171
  /**
152
172
  * Compute the actor for a given environment. The only impurity is the tailscale
153
173
  * shell-out (injectable via `resolvers`), so tests drive every branch explicitly.
@@ -171,7 +191,9 @@ export function computeActor(env = process.env, resolvers = defaultResolvers) {
171
191
  // UNRESOLVED rather than being misattributed to the box's owner.
172
192
  if (!who && !sshRaw)
173
193
  who = resolvers.self();
174
- return actorFromIdentity(who, machineId(), readActors());
194
+ const actor = actorFromIdentity(who, machineId(), readActors());
195
+ const avatarUrl = actorAvatar(actor, resolvers.session());
196
+ return avatarUrl ? { ...actor, avatarUrl } : actor;
175
197
  }
176
198
  let cached;
177
199
  let resolverOverride;
@@ -220,6 +242,8 @@ export function actorEnv(actor) {
220
242
  env.AGENTS_ACTOR_GITHUB = actor.github;
221
243
  if (actor.phoenixId)
222
244
  env.AGENTS_ACTOR_PHOENIX_ID = actor.phoenixId;
245
+ if (actor.avatarUrl)
246
+ env.AGENTS_ACTOR_AVATAR = actor.avatarUrl;
223
247
  if (actor.kind === 'human' && actor.name && actor.email) {
224
248
  env.GIT_AUTHOR_NAME = actor.name;
225
249
  env.GIT_AUTHOR_EMAIL = actor.email;
@@ -48,15 +48,17 @@ export declare function startDeviceAuthorization(): Promise<DeviceAuthorization>
48
48
  export declare function pollDeviceToken(deviceCode: string): Promise<DevicePoll>;
49
49
  export declare function fetchWhoAmI(token?: string): Promise<WhoAmI>;
50
50
  /**
51
- * Best-effort fill of the session's hosted avatar (PHNX-3547): sessions written
52
- * before the CLI tracked avatars have none, so ask `/api/v1/auth/me` and merge
53
- * a hosted `avatar_url` into the persisted session — future publishes then
54
- * stamp the OAuth profile image instead of only a Gravatar hash. A no-op when
55
- * signed out, when an avatar is already stored, or when the server exposes
56
- * none; network/server failures are swallowed (the share attribution falls
57
- * back to Gravatar/initials either way).
51
+ * Keep the session's hosted avatar current (PHNX-3547): merge the `avatar_url`
52
+ * Phoenix ID reports on `/api/v1/auth/me` into the persisted session, so the
53
+ * actor env and share attribution can stamp the profile image instead of only
54
+ * a Gravatar hash. Pass `known` when the caller already fetched `/auth/me`
55
+ * (whoami) — then a changed picture is written too. Without it, a session that
56
+ * already carries a picture is left alone rather than spending a network round
57
+ * trip to re-check it (the share publish path). A no-op when signed out or when
58
+ * the server exposes none; network/server failures are swallowed (the Gravatar
59
+ * fallback covers attribution either way).
58
60
  */
59
- export declare function refreshSessionAvatar(): Promise<void>;
61
+ export declare function refreshSessionAvatar(known?: WhoAmI): Promise<void>;
60
62
  export interface SpaceSummary {
61
63
  id: string;
62
64
  slug: string;
@@ -39,22 +39,26 @@ export function fetchWhoAmI(token) {
39
39
  return phoenixRequest('GET', '/api/v1/auth/me', { token });
40
40
  }
41
41
  /**
42
- * Best-effort fill of the session's hosted avatar (PHNX-3547): sessions written
43
- * before the CLI tracked avatars have none, so ask `/api/v1/auth/me` and merge
44
- * a hosted `avatar_url` into the persisted session — future publishes then
45
- * stamp the OAuth profile image instead of only a Gravatar hash. A no-op when
46
- * signed out, when an avatar is already stored, or when the server exposes
47
- * none; network/server failures are swallowed (the share attribution falls
48
- * back to Gravatar/initials either way).
42
+ * Keep the session's hosted avatar current (PHNX-3547): merge the `avatar_url`
43
+ * Phoenix ID reports on `/api/v1/auth/me` into the persisted session, so the
44
+ * actor env and share attribution can stamp the profile image instead of only
45
+ * a Gravatar hash. Pass `known` when the caller already fetched `/auth/me`
46
+ * (whoami) — then a changed picture is written too. Without it, a session that
47
+ * already carries a picture is left alone rather than spending a network round
48
+ * trip to re-check it (the share publish path). A no-op when signed out or when
49
+ * the server exposes none; network/server failures are swallowed (the Gravatar
50
+ * fallback covers attribution either way).
49
51
  */
50
- export async function refreshSessionAvatar() {
52
+ export async function refreshSessionAvatar(known) {
51
53
  const session = readSession();
52
- if (!session || session.avatarUrl)
54
+ if (!session)
55
+ return;
56
+ if (!known && session.avatarUrl)
53
57
  return;
54
58
  try {
55
- const me = await fetchWhoAmI();
59
+ const me = known ?? (await fetchWhoAmI());
56
60
  const hosted = me.avatar_url?.trim();
57
- if (hosted && /^https:\/\//i.test(hosted)) {
61
+ if (hosted && /^https:\/\//i.test(hosted) && hosted !== session.avatarUrl) {
58
62
  writeSession({ ...session, avatarUrl: hosted });
59
63
  }
60
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.77",
3
+ "version": "1.22.78",
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",