@phnx-labs/agents-cli 1.22.15 → 1.22.17

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +13 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/cloud.d.ts +0 -1
  5. package/dist/commands/cloud.js +19 -185
  6. package/dist/commands/doctor.js +16 -14
  7. package/dist/commands/exec.js +153 -16
  8. package/dist/commands/feed.d.ts +1 -0
  9. package/dist/commands/feed.js +36 -32
  10. package/dist/commands/humans.d.ts +10 -0
  11. package/dist/commands/humans.js +91 -0
  12. package/dist/commands/resume.d.ts +17 -0
  13. package/dist/commands/resume.js +80 -0
  14. package/dist/commands/run-cloud.d.ts +26 -0
  15. package/dist/commands/run-cloud.js +162 -0
  16. package/dist/commands/sessions.d.ts +13 -4
  17. package/dist/commands/sessions.js +37 -19
  18. package/dist/commands/versions.js +4 -2
  19. package/dist/commands/view.js +1 -54
  20. package/dist/index.js +3 -2
  21. package/dist/lib/agents.js +4 -2
  22. package/dist/lib/channels/send.d.ts +5 -2
  23. package/dist/lib/channels/send.js +16 -13
  24. package/dist/lib/cloud/dispatch.d.ts +27 -0
  25. package/dist/lib/cloud/dispatch.js +214 -0
  26. package/dist/lib/devices/doctor-findings.d.ts +14 -0
  27. package/dist/lib/devices/doctor-findings.js +92 -51
  28. package/dist/lib/exec.d.ts +6 -2
  29. package/dist/lib/exec.js +22 -4
  30. package/dist/lib/feed-post.d.ts +2 -2
  31. package/dist/lib/feed-post.js +15 -10
  32. package/dist/lib/hooks.d.ts +4 -2
  33. package/dist/lib/hooks.js +173 -23
  34. package/dist/lib/hosts/remote-cmd.js +8 -0
  35. package/dist/lib/humans.d.ts +26 -0
  36. package/dist/lib/humans.js +73 -0
  37. package/dist/lib/memory.js +2 -1
  38. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  39. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  40. package/dist/lib/migrate.js +112 -4
  41. package/dist/lib/notify.d.ts +4 -4
  42. package/dist/lib/notify.js +4 -3
  43. package/dist/lib/permissions.js +12 -10
  44. package/dist/lib/placement.d.ts +8 -4
  45. package/dist/lib/placement.js +14 -8
  46. package/dist/lib/projects.d.ts +3 -3
  47. package/dist/lib/projects.js +19 -17
  48. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  49. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  50. package/dist/lib/secrets/bundles.d.ts +11 -0
  51. package/dist/lib/secrets/bundles.js +122 -5
  52. package/dist/lib/session/actor-sidecar.d.ts +5 -2
  53. package/dist/lib/session/actor-sidecar.js +4 -2
  54. package/dist/lib/session/db.d.ts +2 -1
  55. package/dist/lib/session/db.js +19 -3
  56. package/dist/lib/session/types.d.ts +4 -0
  57. package/dist/lib/settings-manifest.js +7 -1
  58. package/dist/lib/staleness/writers/sources.d.ts +6 -1
  59. package/dist/lib/staleness/writers/sources.js +93 -7
  60. package/dist/lib/startup/command-registry.d.ts +2 -0
  61. package/dist/lib/startup/command-registry.js +5 -0
  62. package/dist/lib/state.d.ts +2 -0
  63. package/dist/lib/state.js +6 -6
  64. package/dist/lib/types.d.ts +52 -0
  65. package/dist/lib/versions.js +48 -14
  66. package/package.json +1 -1
@@ -10,7 +10,7 @@
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import { getUserAgentsDir, getAgentsDir, getEnabledExtraRepos, getCommandsDir, getSkillsDir, getHooksDir } from '../../state.js';
13
- import { safeJoin } from '../../paths.js';
13
+ import { isSafeSegmentName, safeJoin } from '../../paths.js';
14
14
  /** Trusted source bases for content-like kinds. Project layer excluded. */
15
15
  export function trustedSourceBases() {
16
16
  return [
@@ -119,14 +119,100 @@ export function listPluginSkillNames(options = {}) {
119
119
  }
120
120
  return Array.from(names);
121
121
  }
122
- /** Find the trusted source file for a hook by name. */
122
+ /**
123
+ * Subdirectories under hooks/ that are never group dirs.
124
+ * Must stay in lockstep with HOOK_GROUP_SKIP_DIRS in hooks.ts.
125
+ */
126
+ const HOOK_GROUP_SKIP_DIRS = new Set(['node_modules', '.git', '.cache']);
127
+ const HOOK_SCRIPT_EXTS = new Set([
128
+ '.sh', '.bash', '.zsh', '.py', '.js', '.ts', '.mjs', '.cjs', '.rb', '.pl', '.ps1', '.cmd', '.bat',
129
+ ]);
130
+ function dirHasTopLevelScripts(groupDir) {
131
+ let entries;
132
+ try {
133
+ entries = fs.readdirSync(groupDir);
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ for (const name of entries) {
139
+ if (name.startsWith('.'))
140
+ continue;
141
+ const ext = path.extname(name).toLowerCase();
142
+ if (!HOOK_SCRIPT_EXTS.has(ext))
143
+ continue;
144
+ if (isLiveFile(path.join(groupDir, name)))
145
+ return true;
146
+ }
147
+ return false;
148
+ }
149
+ function findNestedHookFile(hooksRoot, basename) {
150
+ if (!isLiveDir(hooksRoot))
151
+ return null;
152
+ let entries;
153
+ try {
154
+ entries = fs.readdirSync(hooksRoot).sort();
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ for (const name of entries) {
160
+ if (name.startsWith('.') || HOOK_GROUP_SKIP_DIRS.has(name))
161
+ continue;
162
+ const groupDir = path.join(hooksRoot, name);
163
+ if (!isLiveDir(groupDir))
164
+ continue;
165
+ // Only group dirs (those with scripts) contribute nested scripts.
166
+ if (!dirHasTopLevelScripts(groupDir))
167
+ continue;
168
+ const candidate = path.join(groupDir, basename);
169
+ if (isLiveFile(candidate))
170
+ return candidate;
171
+ }
172
+ return null;
173
+ }
174
+ /**
175
+ * Find the trusted source for a hook by name.
176
+ * - File basename (`04-session-identity.sh`) or relative path
177
+ * (`session-starts/04-session-identity.sh`) → script file
178
+ * - Directory basename (`tests`) → directory bundle (fixtures-only etc.)
179
+ */
123
180
  export function resolveHookSource(name) {
124
- const candidates = [
125
- safeJoin(path.join(getUserAgentsDir(), 'hooks'), name),
126
- safeJoin(getHooksDir(), name),
127
- ...getEnabledExtraRepos().map((e) => safeJoin(path.join(e.dir, 'hooks'), name)),
181
+ const roots = [
182
+ path.join(getUserAgentsDir(), 'hooks'),
183
+ getHooksDir(),
184
+ ...getEnabledExtraRepos().map((e) => path.join(e.dir, 'hooks')),
128
185
  ];
129
- return candidates.find(isLiveFile) ?? null;
186
+ const base = path.basename(name);
187
+ for (const hooksRoot of roots) {
188
+ // Exact relative path (top-level or group/name) — multi-segment needs
189
+ // path.join + containment, not safeJoin (single-segment only).
190
+ if (name.includes('/') || name.includes('\\')) {
191
+ const candidate = path.resolve(hooksRoot, name);
192
+ const rootResolved = path.resolve(hooksRoot);
193
+ if ((candidate === rootResolved || candidate.startsWith(rootResolved + path.sep)) &&
194
+ isLiveFile(candidate)) {
195
+ return candidate;
196
+ }
197
+ }
198
+ else if (isSafeSegmentName(name)) {
199
+ try {
200
+ const top = safeJoin(hooksRoot, name);
201
+ // File script first; then directory bundle (e.g. tests/ fixtures).
202
+ if (isLiveFile(top))
203
+ return top;
204
+ if (isLiveDir(top) && !dirHasTopLevelScripts(top))
205
+ return top;
206
+ }
207
+ catch {
208
+ /* invalid segment */
209
+ }
210
+ }
211
+ const nested = findNestedHookFile(hooksRoot, base);
212
+ if (nested)
213
+ return nested;
214
+ }
215
+ return null;
130
216
  }
131
217
  /** All trusted command-skill source roots, used to dedup name collisions for commands-as-skills writes. */
132
218
  export function trustedSkillRoots() {
@@ -46,6 +46,7 @@ export declare const loadRoutines: ModuleLoader;
46
46
  export declare const loadMonitors: ModuleLoader;
47
47
  export declare const loadProjects: ModuleLoader;
48
48
  export declare const loadRun: ModuleLoader;
49
+ export declare const loadResume: ModuleLoader;
49
50
  export declare const loadFork: ModuleLoader;
50
51
  export declare const loadDefaults: ModuleLoader;
51
52
  export declare const loadSet: ModuleLoader;
@@ -104,6 +105,7 @@ export declare const loadShare: ModuleLoader;
104
105
  export declare const loadAudit: ModuleLoader;
105
106
  export declare const loadWebhook: ModuleLoader;
106
107
  export declare const loadFunnel: ModuleLoader;
108
+ export declare const loadHumans: ModuleLoader;
107
109
  /**
108
110
  * Commands whose modules pull in the SQLite-backed session/cloud stack. They are
109
111
  * registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
@@ -24,6 +24,7 @@ export const loadRoutines = async () => (await import('../../commands/routines.j
24
24
  export const loadMonitors = async () => (await import('../../commands/monitors.js')).registerMonitorsCommands;
25
25
  export const loadProjects = async () => (await import('../../commands/projects.js')).registerProjectsCommands;
26
26
  export const loadRun = async () => (await import('../../commands/exec.js')).registerRunCommand;
27
+ export const loadResume = async () => (await import('../../commands/resume.js')).registerResumeCommand;
27
28
  export const loadFork = async () => (await import('../../commands/fork.js')).registerForkCommand;
28
29
  export const loadDefaults = async () => (await import('../../commands/defaults.js')).registerDefaultsCommands;
29
30
  export const loadSet = async () => (await import('../../commands/set.js')).registerSetCommand;
@@ -82,6 +83,7 @@ export const loadShare = async () => (await import('../../commands/share.js')).r
82
83
  export const loadAudit = async () => (await import('../../commands/audit.js')).registerAuditCommands;
83
84
  export const loadWebhook = async () => (await import('../../commands/webhook.js')).registerWebhookCommand;
84
85
  export const loadFunnel = async () => (await import('../../commands/funnel.js')).registerFunnelCommand;
86
+ export const loadHumans = async () => (await import('../../commands/humans.js')).registerHumansCommands;
85
87
  /**
86
88
  * Commands whose modules pull in the SQLite-backed session/cloud stack. They are
87
89
  * registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
@@ -93,6 +95,7 @@ export const loadFunnel = async () => (await import('../../commands/funnel.js'))
93
95
  // same SQLite stack, same post-help registration order as sessions.
94
96
  export const LAZY_COMMAND_NAMES = new Set([
95
97
  'sessions',
98
+ 'resume',
96
99
  'roster',
97
100
  'teams',
98
101
  'cloud',
@@ -145,6 +148,7 @@ export const COMMAND_LOADERS = {
145
148
  monitors: [loadMonitors],
146
149
  projects: [loadProjects],
147
150
  run: [loadRun],
151
+ resume: [loadResume],
148
152
  fork: [loadFork],
149
153
  defaults: [loadDefaults],
150
154
  set: [loadSet],
@@ -221,4 +225,5 @@ export const COMMAND_LOADERS = {
221
225
  audit: [loadAudit],
222
226
  webhook: [loadWebhook],
223
227
  funnel: [loadFunnel],
228
+ humans: [loadHumans],
224
229
  };
@@ -23,6 +23,8 @@
23
23
  * and leaves a back-compat symlink at the old path.
24
24
  */
25
25
  import type { Meta } from './types.js';
26
+ /** Return the absolute path to the humans.yaml file. */
27
+ export declare function getHumansFilePath(): string;
26
28
  /**
27
29
  * Header prepended to every agents.yaml the CLI writes (central and per-device
28
30
  * docs). Carries the yaml-language-server schema hint so editors validate the
package/dist/lib/state.js CHANGED
@@ -60,6 +60,10 @@ const LEGACY_SYSTEM_AGENTS_DIR = path.join(HOME, '.agents-system');
60
60
  const META_FILE = path.join(USER_AGENTS_DIR, 'agents.yaml');
61
61
  /** Legacy location — used only for one-shot migration in readMeta(). */
62
62
  const SYSTEM_META_FILE = path.join(SYSTEM_AGENTS_DIR, 'agents.yaml');
63
+ /** Canonical path for the humans.yaml owner-identity/channel config. */
64
+ const HUMANS_FILE = path.join(USER_AGENTS_DIR, 'humans.yaml');
65
+ /** Return the absolute path to the humans.yaml file. */
66
+ export function getHumansFilePath() { return process.env.AGENTS_HUMANS_FILE ?? HUMANS_FILE; }
63
67
  // ─── System resource dirs ─────────────────────────────────────────────────────
64
68
  const SYSTEM_COMMANDS_DIR = path.join(SYSTEM_AGENTS_DIR, 'commands');
65
69
  const SYSTEM_HOOKS_DIR = path.join(SYSTEM_AGENTS_DIR, 'hooks');
@@ -300,7 +304,7 @@ export function getResolvedRulesDir() { return SYSTEM_RULES_DIR; }
300
304
  /** Path to MCP server YAML configs — system repo. */
301
305
  export function getMcpDir() { return SYSTEM_MCP_DIR; }
302
306
  /** Path to permission group YAML files — system repo. */
303
- export function getPermissionsDir() { return SYSTEM_PERMISSIONS_DIR; }
307
+ export function getPermissionsDir() { return process.env.AGENTS_SYSTEM_PERMISSIONS_DIR ?? SYSTEM_PERMISSIONS_DIR; }
304
308
  /** Path to subagent definition directories — system repo. */
305
309
  export function getSubagentsDir() { return SYSTEM_SUBAGENTS_DIR; }
306
310
  /** Path to ~/.agents/.system/hooks/promptcuts.yaml (system defaults). */
@@ -359,7 +363,7 @@ export function getUserHooksDir() { return USER_HOOKS_DIR; }
359
363
  export function getUserSkillsDir() { return USER_SKILLS_DIR; }
360
364
  export function getUserRulesDir() { return USER_RULES_DIR; }
361
365
  export function getUserMcpDir() { return USER_MCP_DIR; }
362
- export function getUserPermissionsDir() { return USER_PERMISSIONS_DIR; }
366
+ export function getUserPermissionsDir() { return process.env.AGENTS_USER_PERMISSIONS_DIR ?? USER_PERMISSIONS_DIR; }
363
367
  export function getUserSubagentsDir() { return USER_SUBAGENTS_DIR; }
364
368
  export function getSystemWorkflowsDir() { return SYSTEM_WORKFLOWS_DIR; }
365
369
  export function getUserWorkflowsDir() { return USER_WORKFLOWS_DIR; }
@@ -674,8 +678,6 @@ export function ensureAgentsDir() {
674
678
  fs.mkdirSync(PACKAGES_DIR, opts);
675
679
  if (!fs.existsSync(ROUTINES_DIR))
676
680
  fs.mkdirSync(ROUTINES_DIR, opts);
677
- if (!fs.existsSync(WEBHOOKS_DIR))
678
- fs.mkdirSync(WEBHOOKS_DIR, opts);
679
681
  if (!fs.existsSync(RUNS_DIR))
680
682
  fs.mkdirSync(RUNS_DIR, opts);
681
683
  if (!fs.existsSync(VERSIONS_DIR))
@@ -694,8 +696,6 @@ export function ensureAgentsDir() {
694
696
  fs.mkdirSync(SYSTEM_PERMISSIONS_DIR, opts);
695
697
  if (!fs.existsSync(SYSTEM_SUBAGENTS_DIR))
696
698
  fs.mkdirSync(SYSTEM_SUBAGENTS_DIR, opts);
697
- if (!fs.existsSync(SYSTEM_WEBHOOKS_DIR))
698
- fs.mkdirSync(SYSTEM_WEBHOOKS_DIR, opts);
699
699
  try {
700
700
  fs.chmodSync(SYSTEM_AGENTS_DIR, 0o700);
701
701
  }
@@ -1002,6 +1002,58 @@ export interface Meta {
1002
1002
  projects?: string[];
1003
1003
  };
1004
1004
  }
1005
+ /** A single delivery channel entry in humans.yaml. */
1006
+ export interface HumanChannel {
1007
+ /** Canonical channel identifier (e.g. "imessage", "call"). */
1008
+ id: string;
1009
+ /** Provider transport (e.g. "rush", "twilio"). */
1010
+ transport: string;
1011
+ /** Provider-specific recipient (phone number, user id, address). */
1012
+ to?: string;
1013
+ /** If true the channel is watched for incoming messages. */
1014
+ watch?: boolean;
1015
+ /** Shell command to invoke (for call channels). */
1016
+ cmd?: string;
1017
+ /** Credentials bundle name (`agents secrets`). */
1018
+ creds?: string;
1019
+ /** Whether the channel is intrusive (e.g. voice call). */
1020
+ intrusive?: boolean;
1021
+ }
1022
+ /** Severity-to-channel-list escalation policy in humans.yaml. */
1023
+ export interface HumanPolicy {
1024
+ low?: string[];
1025
+ normal?: string[];
1026
+ critical?: string[];
1027
+ }
1028
+ /** Owner identity and contact configuration in humans.yaml. */
1029
+ export interface HumanOwner {
1030
+ /** Display name. */
1031
+ name?: string;
1032
+ /** IANA timezone string (e.g. "America/Los_Angeles"). */
1033
+ timezone?: string;
1034
+ /** Quiet hours as "HH:MM-HH:MM" in local time. */
1035
+ quiet_hours?: string;
1036
+ /** Default notification severity when unspecified. */
1037
+ default_severity?: 'low' | 'normal' | 'critical';
1038
+ /** Short-form channel config for `agents send --to owner` (channel + recipient). */
1039
+ notify?: {
1040
+ channel: string;
1041
+ to: string;
1042
+ };
1043
+ /** Full delivery-channel definitions. */
1044
+ channels?: HumanChannel[];
1045
+ /** Severity-to-channel escalation policy. */
1046
+ policy?: HumanPolicy;
1047
+ }
1048
+ /**
1049
+ * Versioned humans.yaml config — owner identity, channels, and notification
1050
+ * policy. Written to ~/.agents/humans.yaml by `migrateHumans()`.
1051
+ */
1052
+ export interface HumansConfig {
1053
+ /** Schema version. Always 1. */
1054
+ version: 1;
1055
+ owner?: HumanOwner;
1056
+ }
1005
1057
  /** Persisted agent-host entry in agents.yaml (overlay or inline). */
1006
1058
  export interface HostEntry {
1007
1059
  /** `ssh-config`: reach via the bare name (ssh resolves). `inline`: use address/user below. */
@@ -185,15 +185,24 @@ export function getAvailableResources(cwd = process.cwd()) {
185
185
  }
186
186
  }
187
187
  result.skills = filterNamesForActiveResourceProfile('skills', Array.from(skillNames), sourceMapFromResources('skills', cwd));
188
- // Hooks. A hook is either a directory bundle or an actual script file: known
189
- // script extension, OR executable bit on a file with a non-data extension.
190
- // Auxiliary content like `README.md` (docs) or `promptcuts.yaml` (data read
191
- // directly by the expand-promptcuts script) lives in hooks/ but is not a
192
- // hook. Older sync runs chmod 0o755'd everything they copied, so an exec bit
193
- // alone can no longer be trusted as the signal.
188
+ // Hooks:
189
+ // - top-level script files
190
+ // - one-level *group* dirs that contain top-level scripts (session-starts/*.sh)
191
+ // → each script is its own hook resource (install name = basename)
192
+ // - other directories (e.g. tests/ with fixtures only) → directory bundles,
193
+ // copied wholesale as one resource (pre-existing layout)
194
+ // Auxiliary content like README.md / promptcuts.yaml is not a hook. Older sync
195
+ // runs chmod 0o755'd everything, so an exec bit alone is not the signal.
194
196
  const NON_SCRIPT_EXTS = new Set(['.md', '.markdown', '.rst', '.txt', '.yaml', '.yml', '.json', '.toml', '.ini', '.conf']);
195
197
  const SCRIPT_EXTS = new Set(['.sh', '.bash', '.zsh', '.py', '.js', '.ts', '.mjs', '.cjs', '.rb', '.pl', '.ps1']);
198
+ const HOOK_GROUP_SKIP = new Set(['node_modules', '.git', '.cache']);
196
199
  const hookNames = new Set();
200
+ const isHookScriptName = (fileName, mode) => {
201
+ const ext = path.extname(fileName).toLowerCase();
202
+ if (SCRIPT_EXTS.has(ext))
203
+ return true;
204
+ return (mode & 0o111) !== 0 && !NON_SCRIPT_EXTS.has(ext);
205
+ };
197
206
  for (const { base } of resourceBases) {
198
207
  const hooksDir = path.join(base, 'hooks');
199
208
  if (!fs.existsSync(hooksDir))
@@ -202,22 +211,47 @@ export function getAvailableResources(cwd = process.cwd()) {
202
211
  if (name.startsWith('.'))
203
212
  continue;
204
213
  try {
205
- const stat = fs.lstatSync(path.join(hooksDir, name));
214
+ const full = path.join(hooksDir, name);
215
+ const stat = fs.lstatSync(full);
206
216
  if (stat.isSymbolicLink())
207
217
  continue;
208
- if (stat.isDirectory()) {
209
- hookNames.add(name);
218
+ if (stat.isFile()) {
219
+ if (isHookScriptName(name, stat.mode))
220
+ hookNames.add(name);
210
221
  continue;
211
222
  }
212
- if (!stat.isFile())
223
+ if (!stat.isDirectory() || HOOK_GROUP_SKIP.has(name))
213
224
  continue;
214
- const ext = path.extname(name).toLowerCase();
215
- if (SCRIPT_EXTS.has(ext)) {
216
- hookNames.add(name);
225
+ // Group vs bundle: if the dir has any top-level script files, expand
226
+ // them; otherwise treat the whole dir as one hook resource.
227
+ let nestedNames;
228
+ try {
229
+ nestedNames = fs.readdirSync(full);
230
+ }
231
+ catch {
217
232
  continue;
218
233
  }
219
- if ((stat.mode & 0o111) !== 0 && !NON_SCRIPT_EXTS.has(ext))
234
+ const scripts = [];
235
+ for (const nested of nestedNames) {
236
+ if (nested.startsWith('.'))
237
+ continue;
238
+ try {
239
+ const nfull = path.join(full, nested);
240
+ const nstat = fs.lstatSync(nfull);
241
+ if (nstat.isSymbolicLink() || !nstat.isFile())
242
+ continue;
243
+ if (isHookScriptName(nested, nstat.mode))
244
+ scripts.push(nested);
245
+ }
246
+ catch { /* ignore */ }
247
+ }
248
+ if (scripts.length > 0) {
249
+ for (const s of scripts)
250
+ hookNames.add(s);
251
+ }
252
+ else {
220
253
  hookNames.add(name);
254
+ }
221
255
  }
222
256
  catch { /* ignore unreadable */ }
223
257
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.15",
3
+ "version": "1.22.17",
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",