@phnx-labs/agents-cli 1.22.17 → 1.22.19

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,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.19
4
+
5
+ ### Fixed
6
+
7
+ - **`agents sync --local -y` refreshes every installed version, not only the default.**
8
+ Unattended reconcile (`refresh({ skipPrompts })`) previously wrote resources and
9
+ registered hooks into each agent's default version alone, so non-default homes
10
+ kept stale hooks after a system update. Unattended refresh now loops
11
+ `listInstalledVersions` for both resource sync and hook registration.
12
+ Interactive refresh still targets the default only. Source:
13
+ `apps/cli/src/lib/refresh.ts`.
14
+
15
+ ## 1.22.18
16
+
17
+ ### Fixed
18
+
19
+ - **`agents sync` re-copies nested system hooks after content changes.**
20
+ `listResources('hooks')` treated event-group directories (`pre-tool-use/`) as
21
+ resource names, so `system:*` pattern expansion never included nested scripts
22
+ like `git-guard.sh`. Force sync then left stale flat copies in version homes
23
+ forever. Hooks discovery now expands one-level group dirs the same way as
24
+ `getAvailableResources` / `listHookEntriesFromDir`. Source:
25
+ `apps/cli/src/lib/resources.ts`.
26
+
27
+ - Route owner iMessage notifications through Rush's verified owner message endpoint instead of requiring a live daemon channel registration. (RUSH-2193)
28
+
29
+ - **`agents sessions --active` now carries `terminalId` on tmux-hosted rows (RUSH-2192).**
30
+ Grok/Codex (and every `ag-*` tmux pane) get their `AGENT_TERMINAL_ID` from the launch
31
+ registry's by-pid entry. The ps-scan path already set `terminalId`; the tmux source —
32
+ which wins dedupe for interactive agents — omitted it, so Factory could never join a
33
+ tab to its live session even when SessionStart preserved the key. Source:
34
+ `apps/cli/src/lib/session/active.ts`.
35
+
3
36
  ## 1.22.17
4
37
 
5
38
  - **Codex versions no longer share one account — each keeps its own login.** Installing a new Codex version used to copy the current default version's `.codex/auth.json` into the new version home, so `agents view` reported the same ChatGPT account for every installed Codex and you could never sign two versions into two accounts. The credential is now excluded from settings carry-forward (config, prompts, and rules still carry), matching how Claude omits `.claude.json`. A fresh Codex version installs signed-out; run `codex login` (or `agents run codex --version <v>`) inside it to authenticate that version's own account. Source: `apps/cli/src/lib/settings-manifest.ts`, `apps/cli/src/commands/versions.ts`.
package/dist/bin/agents CHANGED
Binary file
@@ -3,4 +3,6 @@ export type RushChannel = 'telegram' | 'imessage' | 'slack' | 'discord';
3
3
  export declare const RUSH_CHANNELS: RushChannel[];
4
4
  /** Build the `rush send` argv (exported for tests). */
5
5
  export declare function buildRushSendArgs(channel: RushChannel, text: string, opts: SendOptions): string[];
6
+ /** Build the owner-scoped iMessage argv. */
7
+ export declare function buildRushOwnerMessageArgs(text: string): string[];
6
8
  export declare const rushProviders: ChannelProvider[];
@@ -1,10 +1,9 @@
1
1
  /**
2
- * Rush-daemon channel providers — telegram / imessage / slack / discord.
2
+ * Rush channel providers — telegram / imessage / slack / discord.
3
3
  *
4
- * These shell out to the already-built `rush send` CLI, which routes through the
5
- * rush daemon's live channel gateways over ~/.rush/daemon.sock. We do NOT import
6
- * rush's Go internals (different repo, internal package) the CLI boundary is
7
- * the contract. `rush send --json` prints {"ok":true,"channel":..,"id":..}.
4
+ * Addressable channels use `rush send`, which routes through the daemon's live
5
+ * gateways. Owner-scoped iMessage uses `rush message send`; it is backed by the
6
+ * verified Rush owner account and does not require a daemon channel registration.
8
7
  */
9
8
  import { execFile } from 'child_process';
10
9
  import { promisify } from 'util';
@@ -19,6 +18,10 @@ export function buildRushSendArgs(channel, text, opts) {
19
18
  args.push('--attachment', a);
20
19
  return args;
21
20
  }
21
+ /** Build the owner-scoped iMessage argv. */
22
+ export function buildRushOwnerMessageArgs(text) {
23
+ return ['message', 'send', '--text', text];
24
+ }
22
25
  function rushProvider(channel) {
23
26
  return {
24
27
  name: channel,
@@ -34,6 +37,18 @@ function rushProvider(channel) {
34
37
  return { ok: false, channel, id: opts.target, error: 'rush CLI not found on PATH' };
35
38
  }
36
39
  try {
40
+ if (channel === 'imessage' && opts.ownerScoped) {
41
+ if ((opts.attachments?.length ?? 0) > 0) {
42
+ return {
43
+ ok: false,
44
+ channel,
45
+ id: opts.target,
46
+ error: 'owner-scoped iMessage does not support attachments',
47
+ };
48
+ }
49
+ await execFileAsync('rush', buildRushOwnerMessageArgs(text));
50
+ return { ok: true, channel, id: opts.target };
51
+ }
37
52
  const { stdout } = await execFileAsync('rush', buildRushSendArgs(channel, text, opts));
38
53
  const parsed = JSON.parse(stdout);
39
54
  return {
@@ -17,6 +17,8 @@ export interface SendOptions {
17
17
  attachments?: string[];
18
18
  /** Sender label (used by the mailbox provider). */
19
19
  from?: string;
20
+ /** Destination was resolved through the verified owner alias. */
21
+ ownerScoped?: boolean;
20
22
  /** Resolve + build the delivery but do not actually send. */
21
23
  dryRun?: boolean;
22
24
  }
@@ -18,6 +18,7 @@ export interface SendEnvelope {
18
18
  thread?: string;
19
19
  attachments?: string[];
20
20
  from?: string;
21
+ ownerScoped?: boolean;
21
22
  dryRun?: boolean;
22
23
  }
23
24
  export interface ResolveSendInput {
@@ -86,6 +86,7 @@ export function resolveSendEnvelope(input, meta) {
86
86
  thread: input.thread?.trim() || undefined,
87
87
  attachments: attachments.length ? attachments : undefined,
88
88
  from: input.from?.trim() || undefined,
89
+ ownerScoped: usedOwnerAlias || (input.ownerMode === true && !input.to?.trim()),
89
90
  dryRun: input.dryRun,
90
91
  },
91
92
  };
@@ -102,6 +103,7 @@ export async function deliverEnvelope(envelope, meta) {
102
103
  thread: envelope.thread,
103
104
  attachments: envelope.attachments,
104
105
  from: envelope.from,
106
+ ownerScoped: envelope.ownerScoped,
105
107
  dryRun: envelope.dryRun,
106
108
  });
107
109
  }
@@ -57,7 +57,11 @@ export async function sendToOwner(text, options = {}) {
57
57
  if (!provider) {
58
58
  return { ok: false, channel, id: target, error };
59
59
  }
60
- return provider.send(text, { target, dryRun: options.dryRun });
60
+ return provider.send(text, {
61
+ target,
62
+ ownerScoped: options.target === undefined,
63
+ dryRun: options.dryRun,
64
+ });
61
65
  }
62
66
  export async function notifyUrgentBlock(block, options = {}) {
63
67
  if (block.notifiedAt) {
@@ -114,7 +114,11 @@ export async function refresh(options = {}) {
114
114
  }
115
115
  }
116
116
  }
117
- // 3. Sync resources to default version homes
117
+ // 3. Sync resources into version homes.
118
+ // Unattended (`skipPrompts` / `agents sync --yes --local`): every installed
119
+ // version — otherwise non-default homes keep stale hooks after a system
120
+ // update (fleet multi-harness: only the default version was refreshed).
121
+ // Interactive: default only; re-run `agents sync <agent>@all` for the rest.
118
122
  const cliStates = await getAllCliStates();
119
123
  const agentsToSync = agentFilter ? [agentFilter] : MANAGED_AGENT_IDS;
120
124
  const available = getAvailableResources();
@@ -124,6 +128,9 @@ export async function refresh(options = {}) {
124
128
  const defaultVer = getGlobalDefault(agentId);
125
129
  if (!defaultVer)
126
130
  continue;
131
+ const versionsToSync = skipPrompts
132
+ ? listInstalledVersions(agentId)
133
+ : [defaultVer];
127
134
  const actuallySynced = getActuallySyncedResources(agentId, defaultVer);
128
135
  const newResources = getNewResources(available, actuallySynced, getProjectOnlyResources());
129
136
  const hasAnySynced = actuallySynced.commands.length > 0 ||
@@ -155,24 +162,29 @@ export async function refresh(options = {}) {
155
162
  forceFullSync = true;
156
163
  }
157
164
  if (forceFullSync || (selection && Object.keys(selection).length > 0)) {
158
- const syncResult = syncResourcesToVersion(agentId, defaultVer, selection, forceFullSync ? { force: true } : undefined);
159
- const synced = [];
160
- if (syncResult.commands)
161
- synced.push('commands');
162
- if (syncResult.skills)
163
- synced.push('skills');
164
- if (syncResult.hooks)
165
- synced.push('hooks');
166
- if (syncResult.memory.length > 0)
167
- synced.push('memory');
168
- if (syncResult.permissions)
169
- synced.push('permissions');
170
- if (syncResult.mcp.length > 0)
171
- synced.push('mcp');
172
- if (syncResult.plugins.length > 0)
173
- synced.push('plugins');
174
- if (synced.length > 0) {
175
- console.log(chalk.green(` Synced: ${synced.join(', ')}`));
165
+ const kinds = new Set();
166
+ for (const ver of versionsToSync) {
167
+ const syncResult = syncResourcesToVersion(agentId, ver, selection, forceFullSync ? { force: true } : undefined);
168
+ if (syncResult.commands)
169
+ kinds.add('commands');
170
+ if (syncResult.skills)
171
+ kinds.add('skills');
172
+ if (syncResult.hooks)
173
+ kinds.add('hooks');
174
+ if (syncResult.memory.length > 0)
175
+ kinds.add('memory');
176
+ if (syncResult.permissions)
177
+ kinds.add('permissions');
178
+ if (syncResult.mcp.length > 0)
179
+ kinds.add('mcp');
180
+ if (syncResult.plugins.length > 0)
181
+ kinds.add('plugins');
182
+ }
183
+ if (kinds.size > 0) {
184
+ const verNote = versionsToSync.length > 1
185
+ ? chalk.gray(` (${versionsToSync.length} versions)`)
186
+ : '';
187
+ console.log(chalk.green(` Synced: ${[...kinds].join(', ')}`) + verNote);
176
188
  }
177
189
  }
178
190
  }
@@ -185,7 +197,7 @@ export async function refresh(options = {}) {
185
197
  }
186
198
  }
187
199
  }
188
- // 4. Register hooks as lifecycle events
200
+ // 4. Register hooks as lifecycle events (same version set as resource sync)
189
201
  const hookManifest = parseHookManifest();
190
202
  if (Object.keys(hookManifest).length > 0) {
191
203
  let hookRegistered = 0;
@@ -195,7 +207,9 @@ export async function refresh(options = {}) {
195
207
  continue;
196
208
  const versions = listInstalledVersions(agentId);
197
209
  const defaultVer = getGlobalDefault(agentId);
198
- const targetVersions = defaultVer ? [defaultVer] : versions.slice(-1);
210
+ const targetVersions = skipPrompts
211
+ ? versions
212
+ : (defaultVer ? [defaultVer] : versions.slice(-1));
199
213
  for (const ver of targetVersions) {
200
214
  const home = getVersionHomePath(agentId, ver);
201
215
  const result = registerHooksToSettings(agentId, home, hookManifest);
@@ -128,6 +128,130 @@ export function listResources(kind, cwd) {
128
128
  [path.join(getSystemAgentsDir(), kind), 'system', getSystemAgentsDir()],
129
129
  ...extraRepos.map((e) => [path.join(e.dir, kind), e.alias, e.dir]),
130
130
  ];
131
+ // Hooks use a one-level event-group layout (hooks/pre-tool-use/git-guard.sh).
132
+ // A flat readdir treats `pre-tool-use` as the resource name, so `system:*`
133
+ // pattern expansion never includes nested scripts — and `agents sync --force`
134
+ // leaves stale flat copies in version homes forever. Mirror getAvailableResources:
135
+ // expand group dirs that hold scripts (install name = basename with extension),
136
+ // keep fixture-only dirs as bundles, and keep top-level scripts as resources.
137
+ // Keep this logic self-contained (no hooks.ts import) so vi.mock of hooks.js
138
+ // in versions tests does not break listResources.
139
+ if (kind === 'hooks') {
140
+ const HOOK_SCRIPT_EXTS = new Set([
141
+ '.sh', '.bash', '.zsh', '.py', '.js', '.ts', '.mjs', '.cjs', '.rb', '.pl', '.ps1', '.cmd', '.bat',
142
+ ]);
143
+ const HOOK_NON_SCRIPT_EXTS = new Set([
144
+ '.md', '.markdown', '.rst', '.txt', '.yaml', '.yml', '.json', '.toml', '.ini', '.conf',
145
+ ]);
146
+ const HOOK_GROUP_SKIP = new Set(['node_modules', '.git', '.cache']);
147
+ const isHookScriptName = (fileName, mode) => {
148
+ const ext = path.extname(fileName).toLowerCase();
149
+ if (HOOK_SCRIPT_EXTS.has(ext))
150
+ return true;
151
+ return (mode & 0o111) !== 0 && !HOOK_NON_SCRIPT_EXTS.has(ext);
152
+ };
153
+ for (const [dir, source, repoRoot] of roots) {
154
+ if (!fs.existsSync(dir))
155
+ continue;
156
+ let top;
157
+ try {
158
+ top = fs.readdirSync(dir);
159
+ }
160
+ catch {
161
+ continue;
162
+ }
163
+ for (const name of top) {
164
+ if (name.startsWith('.'))
165
+ continue;
166
+ const full = path.join(dir, name);
167
+ let stat;
168
+ try {
169
+ stat = fs.lstatSync(full);
170
+ }
171
+ catch {
172
+ continue;
173
+ }
174
+ if (stat.isSymbolicLink())
175
+ continue;
176
+ if (stat.isFile()) {
177
+ if (!isHookScriptName(name, stat.mode))
178
+ continue;
179
+ // Docs that live beside hooks (README/AGENTS) are not resources.
180
+ const raw = name.replace(/\.(md|yaml|yml)$/, '');
181
+ if (isDirectoryDoc(kind, raw))
182
+ continue;
183
+ if (seen.has(name))
184
+ continue;
185
+ if (!resourceIsActive(kind, name, source))
186
+ continue;
187
+ seen.add(name);
188
+ results.push(withProvenance({
189
+ name,
190
+ path: full,
191
+ source,
192
+ repoRoot,
193
+ }));
194
+ continue;
195
+ }
196
+ if (!stat.isDirectory() || HOOK_GROUP_SKIP.has(name))
197
+ continue;
198
+ let nested;
199
+ try {
200
+ nested = fs.readdirSync(full);
201
+ }
202
+ catch {
203
+ continue;
204
+ }
205
+ const scripts = [];
206
+ for (const nestedName of nested) {
207
+ if (nestedName.startsWith('.'))
208
+ continue;
209
+ const nfull = path.join(full, nestedName);
210
+ let nstat;
211
+ try {
212
+ nstat = fs.lstatSync(nfull);
213
+ }
214
+ catch {
215
+ continue;
216
+ }
217
+ if (nstat.isSymbolicLink() || !nstat.isFile())
218
+ continue;
219
+ if (isHookScriptName(nestedName, nstat.mode))
220
+ scripts.push(nestedName);
221
+ }
222
+ if (scripts.length > 0) {
223
+ for (const script of scripts) {
224
+ if (seen.has(script))
225
+ continue;
226
+ if (!resourceIsActive(kind, script, source))
227
+ continue;
228
+ seen.add(script);
229
+ results.push(withProvenance({
230
+ name: script,
231
+ path: path.join(full, script),
232
+ source,
233
+ repoRoot,
234
+ }));
235
+ }
236
+ }
237
+ else {
238
+ // Fixture-only directory bundle (hooks/tests/fixtures/…).
239
+ if (seen.has(name))
240
+ continue;
241
+ if (!resourceIsActive(kind, name, source))
242
+ continue;
243
+ seen.add(name);
244
+ results.push(withProvenance({
245
+ name,
246
+ path: full,
247
+ source,
248
+ repoRoot,
249
+ }));
250
+ }
251
+ }
252
+ }
253
+ return results;
254
+ }
131
255
  for (const [dir, source, repoRoot] of roots) {
132
256
  if (!fs.existsSync(dir))
133
257
  continue;
@@ -1419,12 +1419,16 @@ export async function listTmuxAgentSessions() {
1419
1419
  topic,
1420
1420
  tokPerSec,
1421
1421
  sessionFile,
1422
- // tmux panes carry no start timestamp; derive both from the transcript
1423
- // (creation start, last write last activity).
1424
- startedAtMs: birthtimeMs,
1422
+ // Prefer the launch registry's start when known (more accurate than
1423
+ // transcript birth for a resumed/reused file); else transcript times.
1424
+ startedAtMs: liveEntry?.startedAtMs ?? birthtimeMs,
1425
1425
  lastActivityMs: mtimeMs,
1426
1426
  provenance,
1427
1427
  owner: resolveOwner(liveEntry?.actor, id.sessionId ?? sessionIdFromFile(sessionFile)),
1428
+ // Factory / --active join key: AGENT_TERMINAL_ID stamped on the launch
1429
+ // registry and preserved by SessionStart. Without this, Grok/Codex tmux
1430
+ // panes never surface terminalId even when by-pid has it (RUSH-2192).
1431
+ terminalId: liveEntry?.terminalId,
1428
1432
  // An id-less pane keys its dedupe on the unique pane, so two anonymous
1429
1433
  // co-located panes stay two rows instead of folding into one.
1430
1434
  paneId: id.sessionId ?? sessionIdFromFile(sessionFile) ? undefined : pane,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.17",
3
+ "version": "1.22.19",
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",