@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.
- package/CHANGELOG.md +52 -0
- package/README.md +13 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.d.ts +0 -1
- package/dist/commands/cloud.js +19 -185
- package/dist/commands/doctor.js +16 -14
- package/dist/commands/exec.js +153 -16
- package/dist/commands/feed.d.ts +1 -0
- package/dist/commands/feed.js +36 -32
- package/dist/commands/humans.d.ts +10 -0
- package/dist/commands/humans.js +91 -0
- package/dist/commands/resume.d.ts +17 -0
- package/dist/commands/resume.js +80 -0
- package/dist/commands/run-cloud.d.ts +26 -0
- package/dist/commands/run-cloud.js +162 -0
- package/dist/commands/sessions.d.ts +13 -4
- package/dist/commands/sessions.js +37 -19
- package/dist/commands/versions.js +4 -2
- package/dist/commands/view.js +1 -54
- package/dist/index.js +3 -2
- package/dist/lib/agents.js +4 -2
- package/dist/lib/channels/send.d.ts +5 -2
- package/dist/lib/channels/send.js +16 -13
- package/dist/lib/cloud/dispatch.d.ts +27 -0
- package/dist/lib/cloud/dispatch.js +214 -0
- package/dist/lib/devices/doctor-findings.d.ts +14 -0
- package/dist/lib/devices/doctor-findings.js +92 -51
- package/dist/lib/exec.d.ts +6 -2
- package/dist/lib/exec.js +22 -4
- package/dist/lib/feed-post.d.ts +2 -2
- package/dist/lib/feed-post.js +15 -10
- package/dist/lib/hooks.d.ts +4 -2
- package/dist/lib/hooks.js +173 -23
- package/dist/lib/hosts/remote-cmd.js +8 -0
- package/dist/lib/humans.d.ts +26 -0
- package/dist/lib/humans.js +73 -0
- package/dist/lib/memory.js +2 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.js +112 -4
- package/dist/lib/notify.d.ts +4 -4
- package/dist/lib/notify.js +4 -3
- package/dist/lib/permissions.js +12 -10
- package/dist/lib/placement.d.ts +8 -4
- package/dist/lib/placement.js +14 -8
- package/dist/lib/projects.d.ts +3 -3
- package/dist/lib/projects.js +19 -17
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +11 -0
- package/dist/lib/secrets/bundles.js +122 -5
- package/dist/lib/session/actor-sidecar.d.ts +5 -2
- package/dist/lib/session/actor-sidecar.js +4 -2
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +19 -3
- package/dist/lib/session/types.d.ts +4 -0
- package/dist/lib/settings-manifest.js +7 -1
- package/dist/lib/staleness/writers/sources.d.ts +6 -1
- package/dist/lib/staleness/writers/sources.js +93 -7
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +6 -6
- package/dist/lib/types.d.ts +52 -0
- package/dist/lib/versions.js +48 -14
- package/package.json +1 -1
package/dist/lib/feed-post.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
import { spawnSync } from 'child_process';
|
|
19
|
-
import { appendActivityEvent, } from './activity.js';
|
|
19
|
+
import { appendActivityEvent, readRecentActivity, } from './activity.js';
|
|
20
20
|
import { resolveProjectNameForCwd, listProjectDefs } from './projects.js';
|
|
21
21
|
import { getHistoryDir } from './state.js';
|
|
22
22
|
import { machineId } from './machine-id.js';
|
|
@@ -30,7 +30,7 @@ export const STATUS_TITLE_MAX_CHARS = 60;
|
|
|
30
30
|
* Resolve who is posting. Order:
|
|
31
31
|
* 1. Explicit --session flag
|
|
32
32
|
* 2. Env: AGENT_SESSION_ID / AGENTS_SESSION_ID / basename(AGENTS_MAILBOX_DIR)
|
|
33
|
-
* 3. Env AGENT_LAUNCH_ID → match in pid registry
|
|
33
|
+
* 3. Env AGENT_LAUNCH_ID → match in pid registry or activity index
|
|
34
34
|
* 4. Walk parent PIDs from startPid (default process.ppid) through by-pid registry
|
|
35
35
|
*/
|
|
36
36
|
export function resolvePostIdentity(input) {
|
|
@@ -55,8 +55,12 @@ export function resolvePostIdentity(input) {
|
|
|
55
55
|
registry = walkPidRegistry(start, getParent, readEntry);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
const activity = launchId && !envSession && !registry?.sessionId
|
|
59
|
+
? readRecentActivity({ root: input.activityRoot, maxBytesPerSession: 64 * 1024 })
|
|
60
|
+
.find((event) => event.launchId === launchId)
|
|
61
|
+
: undefined;
|
|
58
62
|
// Prefer env session (explicit + managed run), fill gaps from registry.
|
|
59
|
-
const sessionId = envSession ?? registry?.sessionId;
|
|
63
|
+
const sessionId = envSession ?? registry?.sessionId ?? activity?.sessionId;
|
|
60
64
|
if (!sessionId || !isValidMailboxId(sessionId))
|
|
61
65
|
return undefined;
|
|
62
66
|
const mailboxFromEnv = mailboxIdFromEnv(env);
|
|
@@ -66,17 +70,18 @@ export function resolvePostIdentity(input) {
|
|
|
66
70
|
return {
|
|
67
71
|
sessionId,
|
|
68
72
|
mailboxId,
|
|
69
|
-
host: machineIdFromEnv(env),
|
|
70
|
-
runtime: env.AGENTS_RUNTIME?.trim() || 'headless',
|
|
73
|
+
host: activity?.host ?? machineIdFromEnv(env),
|
|
74
|
+
runtime: env.AGENTS_RUNTIME?.trim() || activity?.runtime || 'headless',
|
|
71
75
|
agent: env.AGENTS_AGENT_NAME?.trim()
|
|
72
76
|
|| registry?.agent
|
|
77
|
+
|| activity?.agent
|
|
73
78
|
|| detectAgentKind(env),
|
|
74
79
|
cwd: input.cwd
|
|
75
|
-
?? (env.AGENTS_CWD?.trim() || registry?.cwd || process.cwd()),
|
|
76
|
-
pid: registry?.pid,
|
|
77
|
-
launchId: launchId || registry?.launchId,
|
|
78
|
-
terminalId: env.AGENT_TERMINAL_ID?.trim() || registry?.terminalId,
|
|
79
|
-
tmuxPane: env.TMUX_PANE?.trim() || registry?.tmuxPane,
|
|
80
|
+
?? (env.AGENTS_CWD?.trim() || registry?.cwd || activity?.cwd || process.cwd()),
|
|
81
|
+
pid: registry?.pid ?? activity?.pid,
|
|
82
|
+
launchId: launchId || registry?.launchId || activity?.launchId,
|
|
83
|
+
terminalId: env.AGENT_TERMINAL_ID?.trim() || registry?.terminalId || activity?.terminalId,
|
|
84
|
+
tmuxPane: env.TMUX_PANE?.trim() || registry?.tmuxPane || activity?.tmuxPane,
|
|
80
85
|
};
|
|
81
86
|
}
|
|
82
87
|
function firstValidId(candidates) {
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -58,8 +58,10 @@ export interface DuplicateVersionHook {
|
|
|
58
58
|
export declare function inspectDuplicateVersionHooks(cwd?: string): DuplicateVersionHook[];
|
|
59
59
|
/**
|
|
60
60
|
* List hook entries in a single directory, grouping script + data files by
|
|
61
|
-
* basename.
|
|
62
|
-
*
|
|
61
|
+
* basename. Also discovers scripts in one-level group subdirs
|
|
62
|
+
* (hooks/session-starts/). Exported so doctor-diff can reuse the same grouping
|
|
63
|
+
* the sync path applies; without this, doctor would double-count `foo.sh` and
|
|
64
|
+
* `foo.yaml`. On basename collision, the top-level file wins over a nested one.
|
|
63
65
|
*/
|
|
64
66
|
export declare function listHookEntriesFromDir(dir: string): HookEntry[];
|
|
65
67
|
/**
|
package/dist/lib/hooks.js
CHANGED
|
@@ -32,12 +32,77 @@ function resolveContainedHookPath(hooksRoot, script) {
|
|
|
32
32
|
return null;
|
|
33
33
|
return resolved;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Subdirectories under hooks/ that group event families (e.g. session-starts/).
|
|
37
|
+
* Scripts one level down are first-class hooks; their install name remains the
|
|
38
|
+
* file basename so version-home copies stay flat and doctor/diff keep matching.
|
|
39
|
+
*/
|
|
40
|
+
const HOOK_GROUP_SKIP_DIRS = new Set(['node_modules', '.git', '.cache']);
|
|
35
41
|
export function resolveHookScriptPath(script) {
|
|
36
42
|
const extraDirs = getEnabledExtraRepos().map(e => e.dir);
|
|
37
43
|
for (const root of [getUserAgentsDir(), ...extraDirs, getSystemAgentsDir()]) {
|
|
38
|
-
const
|
|
44
|
+
const hooksRoot = path.join(root, 'hooks');
|
|
45
|
+
const resolved = resolveContainedHookPath(hooksRoot, script);
|
|
39
46
|
if (resolved)
|
|
40
47
|
return resolved;
|
|
48
|
+
// Basename fallback: manifests may say `session-starts/foo.sh` or just
|
|
49
|
+
// `foo.sh` while the file lives under a one-level group dir.
|
|
50
|
+
const base = path.basename(script);
|
|
51
|
+
const nested = findHookScriptInGroupDirs(hooksRoot, base);
|
|
52
|
+
if (nested)
|
|
53
|
+
return nested;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Find `basename` under hooks/<group>/ (one level). Skips known non-group dirs.
|
|
59
|
+
* Returns the first match in sorted group order for stability.
|
|
60
|
+
*/
|
|
61
|
+
function findHookScriptInGroupDirs(hooksRoot, basename) {
|
|
62
|
+
if (!fs.existsSync(hooksRoot))
|
|
63
|
+
return null;
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = fs.readdirSync(hooksRoot).sort();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
for (const name of entries) {
|
|
72
|
+
if (name.startsWith('.') || HOOK_GROUP_SKIP_DIRS.has(name))
|
|
73
|
+
continue;
|
|
74
|
+
const groupDir = path.join(hooksRoot, name);
|
|
75
|
+
let st;
|
|
76
|
+
try {
|
|
77
|
+
st = fs.lstatSync(groupDir);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!st.isDirectory() || st.isSymbolicLink())
|
|
83
|
+
continue;
|
|
84
|
+
// Only treat dirs that themselves contain scripts as groups (session-starts/),
|
|
85
|
+
// not fixture-only directory bundles (tests/).
|
|
86
|
+
let hasScript = false;
|
|
87
|
+
try {
|
|
88
|
+
for (const child of fs.readdirSync(groupDir)) {
|
|
89
|
+
if (SCRIPT_EXTENSIONS.has(path.extname(child).toLowerCase())) {
|
|
90
|
+
const cp = path.join(groupDir, child);
|
|
91
|
+
if (fs.existsSync(cp) && fs.statSync(cp).isFile()) {
|
|
92
|
+
hasScript = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!hasScript)
|
|
102
|
+
continue;
|
|
103
|
+
const candidate = path.join(groupDir, basename);
|
|
104
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
|
|
105
|
+
return candidate;
|
|
41
106
|
}
|
|
42
107
|
return null;
|
|
43
108
|
}
|
|
@@ -383,38 +448,120 @@ function removeHookFiles(dir, name) {
|
|
|
383
448
|
}
|
|
384
449
|
}
|
|
385
450
|
/**
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
451
|
+
* Collect hook-adjacent files from a hooks root: top-level files plus files in
|
|
452
|
+
* one-level group subdirs (e.g. hooks/session-starts/*.sh). Group dirs exist
|
|
453
|
+
* only for layout; the install/list name stays the file basename so sync and
|
|
454
|
+
* doctor keep a flat version-home model.
|
|
389
455
|
*/
|
|
390
|
-
|
|
391
|
-
if (!fs.existsSync(dir)) {
|
|
392
|
-
return [];
|
|
393
|
-
}
|
|
456
|
+
function collectHookFilesFromRoot(dir) {
|
|
394
457
|
const files = [];
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
if (!stat.isFile())
|
|
399
|
-
continue;
|
|
400
|
-
const ext = path.extname(file);
|
|
401
|
-
const base = path.basename(file, ext);
|
|
458
|
+
const pushFile = (fullPath, fileName, mode) => {
|
|
459
|
+
const ext = path.extname(fileName);
|
|
460
|
+
const base = path.basename(fileName, ext);
|
|
402
461
|
files.push({
|
|
403
|
-
name:
|
|
462
|
+
name: fileName,
|
|
404
463
|
base,
|
|
405
464
|
ext,
|
|
406
465
|
fullPath,
|
|
407
|
-
isExec: isExecutable(
|
|
466
|
+
isExec: isExecutable(mode),
|
|
408
467
|
});
|
|
468
|
+
};
|
|
469
|
+
let top;
|
|
470
|
+
try {
|
|
471
|
+
top = fs.readdirSync(dir);
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return files;
|
|
475
|
+
}
|
|
476
|
+
for (const file of top) {
|
|
477
|
+
if (file.startsWith('.'))
|
|
478
|
+
continue;
|
|
479
|
+
const fullPath = path.join(dir, file);
|
|
480
|
+
let stat;
|
|
481
|
+
try {
|
|
482
|
+
stat = fs.lstatSync(fullPath);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (stat.isSymbolicLink())
|
|
488
|
+
continue;
|
|
489
|
+
if (stat.isFile()) {
|
|
490
|
+
pushFile(fullPath, file, stat.mode);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (!stat.isDirectory() || HOOK_GROUP_SKIP_DIRS.has(file))
|
|
494
|
+
continue;
|
|
495
|
+
// One-level event-group layout: hooks/<group>/<script>. Only dirs that
|
|
496
|
+
// contain top-level scripts are groups; fixture-only dirs (tests/) are not.
|
|
497
|
+
let nested;
|
|
498
|
+
try {
|
|
499
|
+
nested = fs.readdirSync(fullPath);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const nestedFiles = [];
|
|
505
|
+
let hasScript = false;
|
|
506
|
+
for (const nestedName of nested) {
|
|
507
|
+
if (nestedName.startsWith('.'))
|
|
508
|
+
continue;
|
|
509
|
+
const nestedPath = path.join(fullPath, nestedName);
|
|
510
|
+
let nstat;
|
|
511
|
+
try {
|
|
512
|
+
nstat = fs.lstatSync(nestedPath);
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (nstat.isSymbolicLink() || !nstat.isFile())
|
|
518
|
+
continue;
|
|
519
|
+
nestedFiles.push({ nestedName, nestedPath, mode: nstat.mode });
|
|
520
|
+
if (SCRIPT_EXTENSIONS.has(path.extname(nestedName).toLowerCase()))
|
|
521
|
+
hasScript = true;
|
|
522
|
+
}
|
|
523
|
+
if (!hasScript)
|
|
524
|
+
continue;
|
|
525
|
+
for (const n of nestedFiles)
|
|
526
|
+
pushFile(n.nestedPath, n.nestedName, n.mode);
|
|
409
527
|
}
|
|
410
|
-
|
|
528
|
+
return files;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* List hook entries in a single directory, grouping script + data files by
|
|
532
|
+
* basename. Also discovers scripts in one-level group subdirs
|
|
533
|
+
* (hooks/session-starts/). Exported so doctor-diff can reuse the same grouping
|
|
534
|
+
* the sync path applies; without this, doctor would double-count `foo.sh` and
|
|
535
|
+
* `foo.yaml`. On basename collision, the top-level file wins over a nested one.
|
|
536
|
+
*/
|
|
537
|
+
export function listHookEntriesFromDir(dir) {
|
|
538
|
+
if (!fs.existsSync(dir)) {
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
const files = collectHookFilesFromRoot(dir);
|
|
542
|
+
// Prefer top-level over nested when basenames collide (stable install name).
|
|
543
|
+
const byBase = new Map();
|
|
411
544
|
for (const file of files) {
|
|
412
|
-
const list =
|
|
413
|
-
|
|
414
|
-
|
|
545
|
+
const list = byBase.get(file.base) || [];
|
|
546
|
+
// Top-level files sit directly under dir; nested have an extra path segment.
|
|
547
|
+
const isTop = path.dirname(file.fullPath) === path.resolve(dir);
|
|
548
|
+
if (isTop)
|
|
549
|
+
list.unshift(file);
|
|
550
|
+
else
|
|
551
|
+
list.push(file);
|
|
552
|
+
byBase.set(file.base, list);
|
|
415
553
|
}
|
|
416
554
|
const entries = [];
|
|
417
|
-
for (const [base,
|
|
555
|
+
for (const [base, groupAll] of byBase) {
|
|
556
|
+
// Keep only files that share the winning script's directory so a nested
|
|
557
|
+
// data sidecar next to a nested script still pairs, and a top-level
|
|
558
|
+
// winner is not paired with a nested yaml of the same basename.
|
|
559
|
+
const winnerScript = groupAll.find((f) => SCRIPT_EXTENSIONS.has(f.ext.toLowerCase())) ||
|
|
560
|
+
groupAll.find((f) => f.isExec && !NON_SCRIPT_EXTENSIONS.has(f.ext.toLowerCase()));
|
|
561
|
+
if (!winnerScript)
|
|
562
|
+
continue;
|
|
563
|
+
const groupDir = path.dirname(winnerScript.fullPath);
|
|
564
|
+
const group = groupAll.filter((f) => path.dirname(f.fullPath) === groupDir);
|
|
418
565
|
group.sort((a, b) => a.name.localeCompare(b.name));
|
|
419
566
|
// A group is a hook only if it has an actual script: a script extension,
|
|
420
567
|
// OR an executable bit on a file whose extension is not a known data /
|
|
@@ -1268,7 +1415,10 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
1268
1415
|
return resolveContainedHookPath(path.join(overrideRoots[0], 'hooks'), script);
|
|
1269
1416
|
}
|
|
1270
1417
|
if (localHooksDir) {
|
|
1271
|
-
|
|
1418
|
+
// Prefer the exact relative path, then a flat basename copy (sync flattens
|
|
1419
|
+
// group-dir scripts into the version-home hooks/ root).
|
|
1420
|
+
const local = resolveContainedHookPath(localHooksDir, script) ||
|
|
1421
|
+
resolveContainedHookPath(localHooksDir, path.basename(script));
|
|
1272
1422
|
if (local)
|
|
1273
1423
|
return local;
|
|
1274
1424
|
}
|
|
@@ -118,6 +118,14 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
118
118
|
bare: 'local-only', // skips the local setup-copy push; lease-only concern
|
|
119
119
|
tailscale: 'local-only', // --tailscale/--no-tailscale gate the lease net mode; never forwarded
|
|
120
120
|
copyCreds: 'local-only', // copies creds TO the host before dispatch — local concern only
|
|
121
|
+
// Cloud placement: chosen and dispatched from THIS machine via the provider
|
|
122
|
+
// registry; mutually exclusive with --host (placement conflict dies before
|
|
123
|
+
// dispatch), so these never have a remote argv to ride.
|
|
124
|
+
cloud: 'local-only',
|
|
125
|
+
provider: 'local-only',
|
|
126
|
+
repo: 'local-only',
|
|
127
|
+
branch: 'local-only',
|
|
128
|
+
cloudEnv: 'local-only',
|
|
121
129
|
authCheck: 'local-only', // --no-auth-check gates the local interactive login preflight; --host runs skip that preflight entirely
|
|
122
130
|
// The notification must land on the box the PERSON is at — the one that
|
|
123
131
|
// dispatched — not on a headless worker with no desktop to post to. The local
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HumansConfig, HumanOwner } from './types.js';
|
|
2
|
+
export declare const HUMANS_VERSION: 1;
|
|
3
|
+
export declare const HUMANS_HEADER = "# humans.yaml \u2014 owner identity and notification channels\n# Managed by agents-cli. See: agents humans --help\n";
|
|
4
|
+
/**
|
|
5
|
+
* Read and parse humans.yaml. Returns null when the file does not exist or
|
|
6
|
+
* is not a valid v1 config — never throws.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readHumans(): HumansConfig | null;
|
|
9
|
+
/**
|
|
10
|
+
* Write a HumansConfig to ~/.agents/humans.yaml. Creates the file with the
|
|
11
|
+
* canonical header. Overwrites any existing content.
|
|
12
|
+
*/
|
|
13
|
+
export declare function writeHumans(config: HumansConfig): void;
|
|
14
|
+
/**
|
|
15
|
+
* Return the effective owner notification destination from humans.yaml.
|
|
16
|
+
* The owner's normal-severity policy selects the channel; when no normal
|
|
17
|
+
* policy is declared, the first addressable channel is the default.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getOwnerNotifyFromHumans(): {
|
|
20
|
+
channel: string;
|
|
21
|
+
to: string;
|
|
22
|
+
} | null;
|
|
23
|
+
/**
|
|
24
|
+
* Read the owner block from humans.yaml. Returns null if missing.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getOwnerFromHumans(): HumanOwner | null;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* humans.yaml — owner identity, channels, and notification policy.
|
|
3
|
+
*
|
|
4
|
+
* Canonical file: ~/.agents/humans.yaml
|
|
5
|
+
* Schema version: 1
|
|
6
|
+
*
|
|
7
|
+
* This module is the single read/write seam for humans.yaml. All channel/
|
|
8
|
+
* notify consumers should read owner config from here.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as yaml from 'yaml';
|
|
12
|
+
import { getHumansFilePath } from './state.js';
|
|
13
|
+
export const HUMANS_VERSION = 1;
|
|
14
|
+
export const HUMANS_HEADER = `# humans.yaml — owner identity and notification channels
|
|
15
|
+
# Managed by agents-cli. See: agents humans --help
|
|
16
|
+
`;
|
|
17
|
+
/**
|
|
18
|
+
* Read and parse humans.yaml. Returns null when the file does not exist or
|
|
19
|
+
* is not a valid v1 config — never throws.
|
|
20
|
+
*/
|
|
21
|
+
export function readHumans() {
|
|
22
|
+
const filePath = getHumansFilePath();
|
|
23
|
+
if (!fs.existsSync(filePath))
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
27
|
+
const parsed = yaml.parse(raw);
|
|
28
|
+
if (!parsed || typeof parsed !== 'object')
|
|
29
|
+
return null;
|
|
30
|
+
const doc = parsed;
|
|
31
|
+
if (doc['version'] !== HUMANS_VERSION)
|
|
32
|
+
return null;
|
|
33
|
+
return doc;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Write a HumansConfig to ~/.agents/humans.yaml. Creates the file with the
|
|
41
|
+
* canonical header. Overwrites any existing content.
|
|
42
|
+
*/
|
|
43
|
+
export function writeHumans(config) {
|
|
44
|
+
const filePath = getHumansFilePath();
|
|
45
|
+
const body = yaml.stringify(config, { lineWidth: 120 });
|
|
46
|
+
fs.writeFileSync(filePath, HUMANS_HEADER + body, { encoding: 'utf-8', mode: 0o600 });
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Return the effective owner notification destination from humans.yaml.
|
|
50
|
+
* The owner's normal-severity policy selects the channel; when no normal
|
|
51
|
+
* policy is declared, the first addressable channel is the default.
|
|
52
|
+
*/
|
|
53
|
+
export function getOwnerNotifyFromHumans() {
|
|
54
|
+
const owner = readHumans()?.owner;
|
|
55
|
+
const channels = owner?.channels ?? [];
|
|
56
|
+
const preferredIds = owner?.policy?.normal ?? [];
|
|
57
|
+
const preferred = preferredIds
|
|
58
|
+
.map((id) => channels.find((entry) => entry.id === id))
|
|
59
|
+
.find((entry) => entry?.to);
|
|
60
|
+
const selected = preferred ?? channels.find((entry) => entry.to);
|
|
61
|
+
if (selected?.id && selected.to)
|
|
62
|
+
return { channel: selected.id, to: selected.to };
|
|
63
|
+
const migrated = owner?.notify;
|
|
64
|
+
if (migrated?.channel && migrated.to)
|
|
65
|
+
return migrated;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read the owner block from humans.yaml. Returns null if missing.
|
|
70
|
+
*/
|
|
71
|
+
export function getOwnerFromHumans() {
|
|
72
|
+
return readHumans()?.owner ?? null;
|
|
73
|
+
}
|
package/dist/lib/memory.js
CHANGED
|
@@ -46,8 +46,9 @@ export function ensureUserMemoryDir() {
|
|
|
46
46
|
}
|
|
47
47
|
return dir;
|
|
48
48
|
}
|
|
49
|
+
const RULE_FILE_NAMES = new Set(['memory.md', 'agents.md', 'claude.md', 'gemini.md', 'readme.md']);
|
|
49
50
|
function isFactFile(name) {
|
|
50
|
-
return name.endsWith('.md') && name.toLowerCase()
|
|
51
|
+
return name.endsWith('.md') && !RULE_FILE_NAMES.has(name.toLowerCase());
|
|
51
52
|
}
|
|
52
53
|
function slugify(name) {
|
|
53
54
|
return name
|
|
Binary file
|
|
Binary file
|
package/dist/lib/migrate.js
CHANGED
|
@@ -1106,9 +1106,7 @@ function migrateRuntimeToCache() {
|
|
|
1106
1106
|
// releases moved into ~/.agents/.cache/plugins/. See issue #20.
|
|
1107
1107
|
moveDirOnce(path.join(USER_DIR, 'cloud'), path.join(CACHE_DIR, 'cloud'));
|
|
1108
1108
|
moveDirOnce(path.join(USER_DIR, 'drive'), path.join(CACHE_DIR, 'drive'));
|
|
1109
|
-
|
|
1110
|
-
// ~/.agents/terminals/live-terminals.json and would race with the move on
|
|
1111
|
-
// VS Code restart. Leave the path where the extension expects it.
|
|
1109
|
+
moveDirOnce(path.join(USER_DIR, 'terminals'), path.join(CACHE_DIR, 'terminals'));
|
|
1112
1110
|
moveDirOnce(path.join(USER_DIR, 'logs'), path.join(CACHE_DIR, 'logs'));
|
|
1113
1111
|
moveDirOnce(path.join(USER_DIR, 'companion'), path.join(CACHE_DIR, 'companion'));
|
|
1114
1112
|
moveDirOnce(path.join(USER_DIR, 'runtime'), path.join(CACHE_DIR, 'state'));
|
|
@@ -1633,7 +1631,7 @@ function containsOnlyDsStore(dir) {
|
|
|
1633
1631
|
function warnSystemOrphans() {
|
|
1634
1632
|
const SHIPPED_ALLOWLIST = new Set([
|
|
1635
1633
|
// resource directories shipped by the npm package
|
|
1636
|
-
'commands', 'hooks', 'skills', 'rules', 'mcp', 'clis', 'permissions', 'subagents', 'profiles', 'agents', 'routines',
|
|
1634
|
+
'commands', 'hooks', 'skills', 'rules', 'mcp', 'clis', 'permissions', 'subagents', 'profiles', 'agents', 'routines', 'webhooks',
|
|
1637
1635
|
// top-level metadata files
|
|
1638
1636
|
'agents.yaml', 'hooks.yaml', 'README.md', 'CHANGELOG.md',
|
|
1639
1637
|
// git + repo metadata
|
|
@@ -2033,6 +2031,115 @@ export function migrateCliDirToClis(agentsDirs) {
|
|
|
2033
2031
|
fs.renameSync(src, dest);
|
|
2034
2032
|
}
|
|
2035
2033
|
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Migrate owner identity into humans.yaml.
|
|
2036
|
+
*
|
|
2037
|
+
* Sources (both are optional; migration is a no-op when neither exists):
|
|
2038
|
+
* 1. ~/.agents/agents.yaml notify.owner.{channel,to} — short-form notify config.
|
|
2039
|
+
* 2. ~/.agents/owner.md — YAML frontmatter with name/timezone/quiet_hours/channels/policy.
|
|
2040
|
+
*
|
|
2041
|
+
* Writes ~/.agents/humans.yaml (version: 1) when at least one source is
|
|
2042
|
+
* present, then removes the migrated keys. Idempotent: exits immediately when
|
|
2043
|
+
* humans.yaml already exists.
|
|
2044
|
+
*/
|
|
2045
|
+
function migrateHumans() {
|
|
2046
|
+
const humansFile = path.join(USER_DIR, 'humans.yaml');
|
|
2047
|
+
if (fs.existsSync(humansFile))
|
|
2048
|
+
return;
|
|
2049
|
+
const agentsYamlPath = path.join(USER_DIR, 'agents.yaml');
|
|
2050
|
+
const ownerMdPath = path.join(USER_DIR, 'owner.md');
|
|
2051
|
+
let notifyOwner;
|
|
2052
|
+
let ownerName;
|
|
2053
|
+
let ownerTimezone;
|
|
2054
|
+
let ownerQuietHours;
|
|
2055
|
+
let ownerDefaultSeverity;
|
|
2056
|
+
let ownerChannels;
|
|
2057
|
+
let ownerPolicy;
|
|
2058
|
+
// 1. Read notify.owner from agents.yaml.
|
|
2059
|
+
if (fs.existsSync(agentsYamlPath)) {
|
|
2060
|
+
try {
|
|
2061
|
+
const raw = fs.readFileSync(agentsYamlPath, 'utf-8');
|
|
2062
|
+
const doc = yaml.parse(raw);
|
|
2063
|
+
const notify = doc?.['notify'];
|
|
2064
|
+
const owner = notify?.['owner'];
|
|
2065
|
+
const channel = typeof owner?.['channel'] === 'string' ? owner['channel'] : undefined;
|
|
2066
|
+
const to = typeof owner?.['to'] === 'string' ? owner['to'] : undefined;
|
|
2067
|
+
if (channel && to) {
|
|
2068
|
+
notifyOwner = { channel, to };
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
catch { /* best-effort */ }
|
|
2072
|
+
}
|
|
2073
|
+
// 2. Read YAML frontmatter from owner.md.
|
|
2074
|
+
if (fs.existsSync(ownerMdPath)) {
|
|
2075
|
+
try {
|
|
2076
|
+
const raw = fs.readFileSync(ownerMdPath, 'utf-8');
|
|
2077
|
+
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2078
|
+
if (fmMatch) {
|
|
2079
|
+
const fm = yaml.parse(fmMatch[1]);
|
|
2080
|
+
if (fm && typeof fm === 'object') {
|
|
2081
|
+
if (typeof fm['name'] === 'string')
|
|
2082
|
+
ownerName = fm['name'];
|
|
2083
|
+
if (typeof fm['timezone'] === 'string')
|
|
2084
|
+
ownerTimezone = fm['timezone'];
|
|
2085
|
+
if (typeof fm['quiet_hours'] === 'string')
|
|
2086
|
+
ownerQuietHours = fm['quiet_hours'];
|
|
2087
|
+
if (typeof fm['default_severity'] === 'string')
|
|
2088
|
+
ownerDefaultSeverity = fm['default_severity'];
|
|
2089
|
+
if (Array.isArray(fm['channels']))
|
|
2090
|
+
ownerChannels = fm['channels'];
|
|
2091
|
+
if (fm['policy'] && typeof fm['policy'] === 'object')
|
|
2092
|
+
ownerPolicy = fm['policy'];
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
catch { /* best-effort */ }
|
|
2097
|
+
}
|
|
2098
|
+
// Only write if we have at least one piece of owner data.
|
|
2099
|
+
if (!notifyOwner && !ownerName && !ownerTimezone && !ownerQuietHours && !ownerDefaultSeverity && !ownerChannels && !ownerPolicy)
|
|
2100
|
+
return;
|
|
2101
|
+
const humansDoc = { version: 1 };
|
|
2102
|
+
const ownerDoc = {};
|
|
2103
|
+
if (ownerName)
|
|
2104
|
+
ownerDoc['name'] = ownerName;
|
|
2105
|
+
if (ownerTimezone)
|
|
2106
|
+
ownerDoc['timezone'] = ownerTimezone;
|
|
2107
|
+
if (ownerQuietHours)
|
|
2108
|
+
ownerDoc['quiet_hours'] = ownerQuietHours;
|
|
2109
|
+
if (ownerDefaultSeverity)
|
|
2110
|
+
ownerDoc['default_severity'] = ownerDefaultSeverity;
|
|
2111
|
+
if (notifyOwner)
|
|
2112
|
+
ownerDoc['notify'] = notifyOwner;
|
|
2113
|
+
if (ownerChannels)
|
|
2114
|
+
ownerDoc['channels'] = ownerChannels;
|
|
2115
|
+
if (ownerPolicy)
|
|
2116
|
+
ownerDoc['policy'] = ownerPolicy;
|
|
2117
|
+
humansDoc['owner'] = ownerDoc;
|
|
2118
|
+
try {
|
|
2119
|
+
const header = '# humans.yaml — owner identity and notification channels\n# Managed by agents-cli. See: agents humans --help\n';
|
|
2120
|
+
fs.writeFileSync(humansFile, header + yaml.stringify(humansDoc, { lineWidth: 120 }), { encoding: 'utf-8', mode: 0o600 });
|
|
2121
|
+
console.error('Migrated owner config to humans.yaml');
|
|
2122
|
+
}
|
|
2123
|
+
catch (err) {
|
|
2124
|
+
console.error(`humans.yaml migration: could not write (${err.message})`);
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
// Remove notify.owner from agents.yaml after successful migration.
|
|
2128
|
+
if (notifyOwner && fs.existsSync(agentsYamlPath)) {
|
|
2129
|
+
try {
|
|
2130
|
+
const raw = fs.readFileSync(agentsYamlPath, 'utf-8');
|
|
2131
|
+
const doc = yaml.parseDocument(raw);
|
|
2132
|
+
const notifyNode = doc.get('notify');
|
|
2133
|
+
if (notifyNode instanceof yaml.YAMLMap) {
|
|
2134
|
+
notifyNode.delete('owner');
|
|
2135
|
+
if (notifyNode.items.length === 0)
|
|
2136
|
+
doc.delete('notify');
|
|
2137
|
+
}
|
|
2138
|
+
fs.writeFileSync(agentsYamlPath, String(doc), 'utf-8');
|
|
2139
|
+
}
|
|
2140
|
+
catch { /* best-effort — leave the old key if we can't rewrite */ }
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2036
2143
|
/** Run all idempotent migrations. Safe to call multiple times. */
|
|
2037
2144
|
export async function runMigration() {
|
|
2038
2145
|
// MUST run first: every other migrator reads SYSTEM_DIR (the new path).
|
|
@@ -2043,6 +2150,7 @@ export async function runMigration() {
|
|
|
2043
2150
|
cliMigrateDirs.push(projectDotAgents);
|
|
2044
2151
|
migrateCliDirToClis(cliMigrateDirs);
|
|
2045
2152
|
migrateAgentsYaml();
|
|
2153
|
+
migrateHumans();
|
|
2046
2154
|
deleteSystemPromptsJson();
|
|
2047
2155
|
migrateSystemConfigJson();
|
|
2048
2156
|
migratePromptcutsIntoHooks();
|
package/dist/lib/notify.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Every human-facing owner notification (feed urgent-block dispatch, monitor
|
|
5
5
|
* `notify` action, `agents notify`) funnels through the single channel seam:
|
|
6
6
|
* `lookupTransport(channel, meta).provider.send(text, opts)`. The recipient comes
|
|
7
|
-
* from `
|
|
7
|
+
* from `humans.yaml` — never a hardcoded chat id — so changing the
|
|
8
8
|
* owner is honoured by every path at once. `notify.transports` picks the actual
|
|
9
9
|
* provider per host (rush telegram on zion, openclaw-telegram on mac-mini).
|
|
10
10
|
* Best-effort: a delivery failure is returned to the caller, never thrown, so a
|
|
@@ -19,9 +19,9 @@ import type { SendResult } from './channels/registry.js';
|
|
|
19
19
|
export interface OwnerNotifyOptions {
|
|
20
20
|
/** Config source (defaults to `readMeta()`); lets callers/tests inject it. */
|
|
21
21
|
meta?: Meta;
|
|
22
|
-
/** Override the owner channel from
|
|
22
|
+
/** Override the owner channel resolved from humans.yaml. */
|
|
23
23
|
channel?: string;
|
|
24
|
-
/** Override the owner target from
|
|
24
|
+
/** Override the owner target resolved from humans.yaml. */
|
|
25
25
|
target?: string;
|
|
26
26
|
/** Resolve + build the delivery but do not actually send. */
|
|
27
27
|
dryRun?: boolean;
|
|
@@ -44,7 +44,7 @@ export declare function buildOpenClawNotifyArgs(text: string, opts: {
|
|
|
44
44
|
}): string[];
|
|
45
45
|
/**
|
|
46
46
|
* Deliver a message to the configured owner through the one channel seam.
|
|
47
|
-
* `channel`/`target` default to
|
|
47
|
+
* `channel`/`target` default to the normal owner channel in humans.yaml; `notify.transports`
|
|
48
48
|
* selects the provider per host. A missing owner config or a delivery failure
|
|
49
49
|
* (e.g. openclaw not on PATH) returns a clean `SendResult` error — never a raw
|
|
50
50
|
* ENOENT — so callers surface a consistent, best-effort failure.
|
package/dist/lib/notify.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readMeta } from './state.js';
|
|
2
|
+
import { getOwnerNotifyFromHumans } from './humans.js';
|
|
2
3
|
import { registerBuiltinProviders } from './channels/providers/index.js';
|
|
3
4
|
import { lookupTransport } from './channels/resolve.js';
|
|
4
5
|
export function formatUrgentBlockMessage(block) {
|
|
@@ -33,14 +34,14 @@ export function buildOpenClawNotifyArgs(text, opts) {
|
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
36
|
* Deliver a message to the configured owner through the one channel seam.
|
|
36
|
-
* `channel`/`target` default to
|
|
37
|
+
* `channel`/`target` default to the normal owner channel in humans.yaml; `notify.transports`
|
|
37
38
|
* selects the provider per host. A missing owner config or a delivery failure
|
|
38
39
|
* (e.g. openclaw not on PATH) returns a clean `SendResult` error — never a raw
|
|
39
40
|
* ENOENT — so callers surface a consistent, best-effort failure.
|
|
40
41
|
*/
|
|
41
42
|
export async function sendToOwner(text, options = {}) {
|
|
42
43
|
const meta = options.meta ?? readMeta();
|
|
43
|
-
const owner = meta.notify?.owner;
|
|
44
|
+
const owner = getOwnerNotifyFromHumans() ?? meta.notify?.owner;
|
|
44
45
|
const channel = options.channel ?? owner?.channel;
|
|
45
46
|
const target = options.target ?? owner?.to;
|
|
46
47
|
if (!channel || !target) {
|
|
@@ -48,7 +49,7 @@ export async function sendToOwner(text, options = {}) {
|
|
|
48
49
|
ok: false,
|
|
49
50
|
channel: channel ?? 'unknown',
|
|
50
51
|
id: target ?? '',
|
|
51
|
-
error: '
|
|
52
|
+
error: 'No addressable owner channel configured in humans.yaml or legacy notify.owner',
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
registerBuiltinProviders();
|