@phnx-labs/agents-cli 1.22.14 → 1.22.16
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 +53 -0
- package/README.md +7 -5
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.js +4 -6
- package/dist/commands/exec.js +96 -11
- package/dist/commands/humans.d.ts +10 -0
- package/dist/commands/humans.js +91 -0
- package/dist/commands/packages.js +90 -22
- package/dist/commands/projects.d.ts +2 -5
- package/dist/commands/projects.js +92 -65
- package/dist/commands/resume.d.ts +17 -0
- package/dist/commands/resume.js +80 -0
- package/dist/commands/routines.js +53 -71
- package/dist/commands/sessions.d.ts +13 -4
- package/dist/commands/sessions.js +37 -19
- package/dist/commands/setup-watchdog.d.ts +3 -0
- package/dist/commands/setup-watchdog.js +54 -0
- package/dist/commands/setup.js +8 -1
- package/dist/commands/ssh.d.ts +11 -0
- package/dist/commands/ssh.js +18 -4
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +40 -28
- package/dist/index.js +3 -2
- package/dist/lib/auto-dispatch.d.ts +8 -10
- package/dist/lib/auto-dispatch.js +20 -42
- package/dist/lib/channels/send.d.ts +4 -1
- package/dist/lib/channels/send.js +15 -7
- 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/hooks.d.ts +4 -2
- package/dist/lib/hooks.js +173 -23
- package/dist/lib/humans.d.ts +25 -0
- package/dist/lib/humans.js +65 -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.d.ts +1 -1
- package/dist/lib/migrate.js +116 -7
- package/dist/lib/notify.js +6 -2
- package/dist/lib/permissions.js +12 -10
- package/dist/lib/project-import.d.ts +7 -46
- package/dist/lib/project-import.js +9 -108
- package/dist/lib/projects.d.ts +20 -3
- package/dist/lib/projects.js +36 -18
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +11 -0
- package/dist/lib/routine-activation.d.ts +14 -0
- package/dist/lib/routine-activation.js +78 -0
- package/dist/lib/routines-project.d.ts +1 -1
- package/dist/lib/routines-project.js +3 -9
- package/dist/lib/routines.d.ts +3 -1
- package/dist/lib/routines.js +32 -19
- 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/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 +23 -10
- package/dist/lib/types.d.ts +64 -1
- package/dist/lib/versions.js +48 -14
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
- package/dist/lib/watchdog/routine.d.ts +0 -44
- package/dist/lib/watchdog/routine.js +0 -69
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
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 config from humans.yaml if
|
|
16
|
+
* available, otherwise return null (caller falls back to agents.yaml).
|
|
17
|
+
*/
|
|
18
|
+
export declare function getOwnerNotifyFromHumans(): {
|
|
19
|
+
channel: string;
|
|
20
|
+
to: string;
|
|
21
|
+
} | null;
|
|
22
|
+
/**
|
|
23
|
+
* Read the owner block from humans.yaml. Returns null if missing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getOwnerFromHumans(): HumanOwner | null;
|
|
@@ -0,0 +1,65 @@
|
|
|
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 (with a fallback to
|
|
9
|
+
* the legacy notify.owner in agents.yaml during the migration window).
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as yaml from 'yaml';
|
|
13
|
+
import { getHumansFilePath } from './state.js';
|
|
14
|
+
export const HUMANS_VERSION = 1;
|
|
15
|
+
export const HUMANS_HEADER = `# humans.yaml — owner identity and notification channels
|
|
16
|
+
# Managed by agents-cli. See: agents humans --help
|
|
17
|
+
`;
|
|
18
|
+
/**
|
|
19
|
+
* Read and parse humans.yaml. Returns null when the file does not exist or
|
|
20
|
+
* is not a valid v1 config — never throws.
|
|
21
|
+
*/
|
|
22
|
+
export function readHumans() {
|
|
23
|
+
const filePath = getHumansFilePath();
|
|
24
|
+
if (!fs.existsSync(filePath))
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
28
|
+
const parsed = yaml.parse(raw);
|
|
29
|
+
if (!parsed || typeof parsed !== 'object')
|
|
30
|
+
return null;
|
|
31
|
+
const doc = parsed;
|
|
32
|
+
if (doc['version'] !== HUMANS_VERSION)
|
|
33
|
+
return null;
|
|
34
|
+
return doc;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Write a HumansConfig to ~/.agents/humans.yaml. Creates the file with the
|
|
42
|
+
* canonical header. Overwrites any existing content.
|
|
43
|
+
*/
|
|
44
|
+
export function writeHumans(config) {
|
|
45
|
+
const filePath = getHumansFilePath();
|
|
46
|
+
const body = yaml.stringify(config, { lineWidth: 120 });
|
|
47
|
+
fs.writeFileSync(filePath, HUMANS_HEADER + body, { encoding: 'utf-8', mode: 0o600 });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Return the effective owner notification config from humans.yaml if
|
|
51
|
+
* available, otherwise return null (caller falls back to agents.yaml).
|
|
52
|
+
*/
|
|
53
|
+
export function getOwnerNotifyFromHumans() {
|
|
54
|
+
const humans = readHumans();
|
|
55
|
+
const notify = humans?.owner?.notify;
|
|
56
|
+
if (notify?.channel && notify?.to)
|
|
57
|
+
return notify;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read the owner block from humans.yaml. Returns null if missing.
|
|
62
|
+
*/
|
|
63
|
+
export function getOwnerFromHumans() {
|
|
64
|
+
return readHumans()?.owner ?? null;
|
|
65
|
+
}
|
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.d.ts
CHANGED
|
@@ -132,7 +132,7 @@ export declare function migrateRoutineDeviceToDevices(routinesDir?: string): voi
|
|
|
132
132
|
* Both seams are injectable so the migration is unit-testable without touching the
|
|
133
133
|
* real routines dir (and without racing the routine module's own tests).
|
|
134
134
|
*/
|
|
135
|
-
export declare function migrateWatchdogSentinelToRoutine(sentinelPath?: string,
|
|
135
|
+
export declare function migrateWatchdogSentinelToRoutine(sentinelPath?: string, enable?: (name: string, enabled: boolean) => void): void;
|
|
136
136
|
/**
|
|
137
137
|
* Rename cli/ → clis/ in each of the given `.agents/` directories.
|
|
138
138
|
*
|
package/dist/lib/migrate.js
CHANGED
|
@@ -13,7 +13,7 @@ import { atomicWriteFileSync } from './fs-atomic.js';
|
|
|
13
13
|
import { machineId } from './machine-id.js';
|
|
14
14
|
import { AGENTS, agentConfigDirName, findInPath } from './agents.js';
|
|
15
15
|
import { createLink } from './platform/index.js';
|
|
16
|
-
import {
|
|
16
|
+
import { migrateLegacyRoutineActivation, setJobEnabled } from './routines.js';
|
|
17
17
|
const HOME = process.env.HOME ?? os.homedir();
|
|
18
18
|
const USER_DIR = path.join(HOME, '.agents');
|
|
19
19
|
/** Canonical system-repo location (post-fold). */
|
|
@@ -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
|
|
@@ -1997,11 +1995,11 @@ export function migrateRoutineDeviceToDevices(routinesDir) {
|
|
|
1997
1995
|
* Both seams are injectable so the migration is unit-testable without touching the
|
|
1998
1996
|
* real routines dir (and without racing the routine module's own tests).
|
|
1999
1997
|
*/
|
|
2000
|
-
export function migrateWatchdogSentinelToRoutine(sentinelPath = path.join(CACHE_DIR, 'state', 'watchdog', 'enabled'),
|
|
1998
|
+
export function migrateWatchdogSentinelToRoutine(sentinelPath = path.join(CACHE_DIR, 'state', 'watchdog', 'enabled'), enable = setJobEnabled) {
|
|
2001
1999
|
if (!fs.existsSync(sentinelPath))
|
|
2002
2000
|
return;
|
|
2003
2001
|
try {
|
|
2004
|
-
|
|
2002
|
+
enable('watchdog', true);
|
|
2005
2003
|
}
|
|
2006
2004
|
catch (err) {
|
|
2007
2005
|
console.error(`watchdog sentinel migration: could not create the routine (${err.message}); leaving the sentinel for a later retry`);
|
|
@@ -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();
|
|
@@ -2096,6 +2204,7 @@ export async function runMigration() {
|
|
|
2096
2204
|
migrateExtrasExtrasToAgentsExtras();
|
|
2097
2205
|
// Rewrite routine YAML files: singular `device:` -> plural `devices: []`.
|
|
2098
2206
|
migrateRoutineDeviceToDevices();
|
|
2207
|
+
migrateLegacyRoutineActivation();
|
|
2099
2208
|
// Fold the legacy watchdog enable sentinel into the watchdog routine so a user
|
|
2100
2209
|
// who opted in under the old build stays opted in after upgrading. After the
|
|
2101
2210
|
// routine rewrites above so the routines dir is in its canonical shape.
|
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) {
|
|
@@ -40,7 +41,10 @@ export function buildOpenClawNotifyArgs(text, opts) {
|
|
|
40
41
|
*/
|
|
41
42
|
export async function sendToOwner(text, options = {}) {
|
|
42
43
|
const meta = options.meta ?? readMeta();
|
|
43
|
-
|
|
44
|
+
// humans.yaml is the primary source; agents.yaml notify.owner is the fallback.
|
|
45
|
+
const humansOwner = getOwnerNotifyFromHumans();
|
|
46
|
+
const legacyOwner = meta.notify?.owner;
|
|
47
|
+
const owner = humansOwner ?? legacyOwner;
|
|
44
48
|
const channel = options.channel ?? owner?.channel;
|
|
45
49
|
const target = options.target ?? owner?.to;
|
|
46
50
|
if (!channel || !target) {
|
|
@@ -48,7 +52,7 @@ export async function sendToOwner(text, options = {}) {
|
|
|
48
52
|
ok: false,
|
|
49
53
|
channel: channel ?? 'unknown',
|
|
50
54
|
id: target ?? '',
|
|
51
|
-
error: 'notify.owner.{channel,to} not set in agents.yaml',
|
|
55
|
+
error: 'notify.owner.{channel,to} not set in humans.yaml or agents.yaml',
|
|
52
56
|
};
|
|
53
57
|
}
|
|
54
58
|
registerBuiltinProviders();
|
package/dist/lib/permissions.js
CHANGED
|
@@ -122,9 +122,9 @@ export function convertDenyToCodexRules(deny) {
|
|
|
122
122
|
* Ensure central permissions directory exists.
|
|
123
123
|
*/
|
|
124
124
|
function ensurePermissionsDir() {
|
|
125
|
-
const
|
|
126
|
-
if (!fs.existsSync(
|
|
127
|
-
fs.mkdirSync(
|
|
125
|
+
const groupsDir = path.join(getUserPermissionsDir(), 'groups');
|
|
126
|
+
if (!fs.existsSync(groupsDir)) {
|
|
127
|
+
fs.mkdirSync(groupsDir, { recursive: true });
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
@@ -350,7 +350,8 @@ export function listInstalledPermissions() {
|
|
|
350
350
|
ensureAgentsDir();
|
|
351
351
|
const seen = new Set();
|
|
352
352
|
const results = [];
|
|
353
|
-
for (const
|
|
353
|
+
for (const baseDir of [getUserPermissionsDir(), getPermissionsDir()]) {
|
|
354
|
+
const dir = path.join(baseDir, 'groups');
|
|
354
355
|
if (!fs.existsSync(dir))
|
|
355
356
|
continue;
|
|
356
357
|
try {
|
|
@@ -377,10 +378,11 @@ export function listInstalledPermissions() {
|
|
|
377
378
|
return results;
|
|
378
379
|
}
|
|
379
380
|
/**
|
|
380
|
-
* Get a specific permission set by name. Searches user dir first, then system
|
|
381
|
+
* Get a specific permission set by name. Searches user groups/ dir first, then system groups/.
|
|
381
382
|
*/
|
|
382
383
|
function getPermissionSet(name) {
|
|
383
|
-
for (const
|
|
384
|
+
for (const baseDir of [getUserPermissionsDir(), getPermissionsDir()]) {
|
|
385
|
+
const dir = path.join(baseDir, 'groups');
|
|
384
386
|
for (const ext of ['.yml', '.yaml']) {
|
|
385
387
|
const filePath = safeJoin(dir, name + ext);
|
|
386
388
|
if (fs.existsSync(filePath)) {
|
|
@@ -402,7 +404,7 @@ export function installPermissionSet(sourcePath, name) {
|
|
|
402
404
|
if (!set) {
|
|
403
405
|
return { success: false, error: 'Invalid permission file' };
|
|
404
406
|
}
|
|
405
|
-
const targetPath = safeJoin(getUserPermissionsDir(), name + '.yml');
|
|
407
|
+
const targetPath = safeJoin(path.join(getUserPermissionsDir(), 'groups'), name + '.yml');
|
|
406
408
|
try {
|
|
407
409
|
fs.copyFileSync(sourcePath, targetPath);
|
|
408
410
|
return { success: true };
|
|
@@ -416,9 +418,9 @@ export function installPermissionSet(sourcePath, name) {
|
|
|
416
418
|
* sets are intentionally not deletable from user commands.
|
|
417
419
|
*/
|
|
418
420
|
export function removePermissionSet(name) {
|
|
419
|
-
const
|
|
421
|
+
const groupsDir = path.join(getUserPermissionsDir(), 'groups');
|
|
420
422
|
for (const ext of ['.yml', '.yaml']) {
|
|
421
|
-
const filePath = safeJoin(
|
|
423
|
+
const filePath = safeJoin(groupsDir, name + ext);
|
|
422
424
|
if (fs.existsSync(filePath)) {
|
|
423
425
|
try {
|
|
424
426
|
fs.unlinkSync(filePath);
|
|
@@ -2025,7 +2027,7 @@ export function exportPermissionsFromPath(filePath) {
|
|
|
2025
2027
|
*/
|
|
2026
2028
|
function savePermissionSet(set) {
|
|
2027
2029
|
ensurePermissionsDir();
|
|
2028
|
-
const filePath = safeJoin(getUserPermissionsDir(), set.name + '.yml');
|
|
2030
|
+
const filePath = safeJoin(path.join(getUserPermissionsDir(), 'groups'), set.name + '.yml');
|
|
2029
2031
|
try {
|
|
2030
2032
|
const content = yaml.stringify({
|
|
2031
2033
|
name: set.name,
|