@phnx-labs/agents-cli 1.22.22 → 1.22.24
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 +402 -0
- package/README.md +13 -6
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.js +20 -7
- package/dist/commands/exec.js +100 -34
- package/dist/commands/feed.d.ts +18 -0
- package/dist/commands/feed.js +44 -1
- package/dist/commands/focus.d.ts +108 -5
- package/dist/commands/focus.js +455 -32
- package/dist/commands/fork.d.ts +20 -2
- package/dist/commands/fork.js +91 -64
- package/dist/commands/go.d.ts +39 -1
- package/dist/commands/go.js +112 -7
- package/dist/commands/harness-wizard.d.ts +206 -0
- package/dist/commands/harness-wizard.js +403 -0
- package/dist/commands/harness.d.ts +12 -0
- package/dist/commands/harness.js +97 -101
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +6 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/resume.js +10 -3
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +79 -47
- package/dist/commands/sessions-browser.d.ts +35 -0
- package/dist/commands/sessions-browser.js +140 -14
- package/dist/commands/sessions-resume.d.ts +23 -3
- package/dist/commands/sessions-resume.js +73 -19
- package/dist/commands/sessions.d.ts +91 -5
- package/dist/commands/sessions.js +359 -125
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/view.js +2 -0
- package/dist/commands/watchdog.js +13 -2
- package/dist/index.js +2 -1
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/agents.d.ts +1 -1
- package/dist/lib/agents.js +132 -0
- package/dist/lib/codex-policy.d.ts +17 -0
- package/dist/lib/codex-policy.js +48 -0
- package/dist/lib/crabbox/lease.d.ts +25 -0
- package/dist/lib/crabbox/lease.js +62 -0
- package/dist/lib/daemon.js +70 -0
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +175 -62
- package/dist/lib/feed-broadcast.d.ts +1 -20
- package/dist/lib/feed-broadcast.js +31 -1
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +100 -40
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/mcp.js +44 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/models.d.ts +0 -5
- package/dist/lib/models.js +48 -0
- package/dist/lib/plugin-marketplace.js +9 -0
- package/dist/lib/pricing/prices.json +119 -92
- package/dist/lib/pricing/table.js +13 -0
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/remote-agents-json.d.ts +29 -1
- package/dist/lib/remote-agents-json.js +47 -10
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources/mcp.js +2 -0
- package/dist/lib/resources/permissions.js +3 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/resources.js +8 -5
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +50 -36
- 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.js +157 -65
- package/dist/lib/secrets/filestore.d.ts +5 -3
- package/dist/lib/secrets/filestore.js +12 -8
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +148 -53
- package/dist/lib/secrets/reaper.d.ts +97 -0
- package/dist/lib/secrets/reaper.js +219 -0
- package/dist/lib/secrets/remote.d.ts +29 -0
- package/dist/lib/secrets/remote.js +37 -1
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/active.d.ts +36 -1
- package/dist/lib/session/active.js +60 -19
- package/dist/lib/session/actor-sidecar.d.ts +14 -0
- package/dist/lib/session/actor-sidecar.js +67 -4
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +32 -1
- package/dist/lib/session/discover.js +168 -0
- package/dist/lib/session/parse.d.ts +10 -0
- package/dist/lib/session/parse.js +98 -0
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- package/dist/lib/session/remote-list.d.ts +10 -1
- package/dist/lib/session/remote-list.js +2 -8
- package/dist/lib/session/remote.d.ts +57 -3
- package/dist/lib/session/remote.js +90 -26
- package/dist/lib/session/resume-command.d.ts +6 -0
- package/dist/lib/session/resume-command.js +8 -0
- package/dist/lib/session/session-cache.d.ts +173 -0
- package/dist/lib/session/session-cache.js +399 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/width.d.ts +1 -1
- package/dist/lib/session/width.js +12 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +62 -8
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +3 -1
- package/dist/lib/usage.d.ts +13 -0
- package/dist/lib/usage.js +215 -0
- package/dist/lib/versions.js +13 -2
- package/package.json +1 -1
package/dist/lib/hooks/cache.js
CHANGED
|
@@ -463,6 +463,9 @@ except Exception: pass' 2>/dev/null || true)"
|
|
|
463
463
|
;;
|
|
464
464
|
esac
|
|
465
465
|
CACHE_FILE="$CACHE_DIR/$HOOK_NAME$cache_suffix.out"
|
|
466
|
+
# Failure sentinel: touched on bg-refresh failure; mtime drives the backoff window.
|
|
467
|
+
FAIL_FILE="$CACHE_FILE.fail"
|
|
468
|
+
BACKOFF_SEC=60
|
|
466
469
|
|
|
467
470
|
# Monotonic-ish nanosecond timer (macOS \`date\` has no %N).
|
|
468
471
|
now_ns() { "$PY" -c 'import time; print(int(time.time()*1e9))'; }
|
|
@@ -491,9 +494,38 @@ if [ "$CACHE_STATUS" = miss ]; then
|
|
|
491
494
|
# Stale-while-revalidate: serve stale immediately, refresh in detached child.
|
|
492
495
|
cat "$CACHE_FILE"
|
|
493
496
|
CACHE_STATUS=stale-prefetch
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
+
# Fix 2: backoff — skip bg refresh if last attempt failed within BACKOFF_SEC.
|
|
498
|
+
_in_backoff=0
|
|
499
|
+
if [ -f "$FAIL_FILE" ]; then
|
|
500
|
+
_fail_mtime=$("$PY" -c 'import os,sys; print(int(os.path.getmtime(sys.argv[1])))' "$FAIL_FILE" 2>/dev/null || echo 0)
|
|
501
|
+
_now_s=$(date +%s)
|
|
502
|
+
_fail_age=$((_now_s - \${_fail_mtime:-0}))
|
|
503
|
+
[ "$_fail_age" -lt "$BACKOFF_SEC" ] && _in_backoff=1
|
|
504
|
+
fi
|
|
505
|
+
if [ "$_in_backoff" -eq 0 ]; then
|
|
506
|
+
# Fix 1: lockdir — only one background refresh runs at a time.
|
|
507
|
+
LOCK_DIR="$CACHE_FILE.bg.lck"
|
|
508
|
+
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
509
|
+
tmp="$CACHE_FILE.new.$$"
|
|
510
|
+
# Fix 3: background subshell captures and logs its own real exit code.
|
|
511
|
+
(
|
|
512
|
+
trap 'rm -rf "$LOCK_DIR"' EXIT
|
|
513
|
+
_bg_exit=0
|
|
514
|
+
if printf '%s' "$STDIN_PAYLOAD" | "$SOURCE" >"$tmp" 2>/dev/null; then
|
|
515
|
+
mv -f "$tmp" "$CACHE_FILE" && rm -f "$FAIL_FILE"
|
|
516
|
+
else
|
|
517
|
+
_bg_exit=$?
|
|
518
|
+
rm -f "$tmp"
|
|
519
|
+
touch "$FAIL_FILE"
|
|
520
|
+
fi
|
|
521
|
+
_bg_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
522
|
+
_bg_log_file="$LOGS_DIR/events-$(date -u +%Y-%m-%d).jsonl"
|
|
523
|
+
printf '{"ts":"%s","event":"hook.cache.refresh","hook":"%s","cache":"bg-refresh","exit":%d}\\n' \\
|
|
524
|
+
"$_bg_ts" "$HOOK_NAME" "$_bg_exit" >>"$_bg_log_file" 2>/dev/null || true
|
|
525
|
+
) >/dev/null 2>&1 &
|
|
526
|
+
disown 2>/dev/null || true
|
|
527
|
+
fi
|
|
528
|
+
fi
|
|
497
529
|
else
|
|
498
530
|
# Synchronous fetch + cache.
|
|
499
531
|
tmp="$CACHE_FILE.new.$$"
|
|
@@ -501,6 +533,7 @@ if [ "$CACHE_STATUS" = miss ]; then
|
|
|
501
533
|
EXIT=0
|
|
502
534
|
cat "$tmp"
|
|
503
535
|
mv -f "$tmp" "$CACHE_FILE"
|
|
536
|
+
rm -f "$FAIL_FILE"
|
|
504
537
|
else
|
|
505
538
|
EXIT=$?
|
|
506
539
|
rm -f "$tmp"
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -73,6 +73,14 @@ export declare function hookExists(agentId: AgentId, hookName: string): boolean;
|
|
|
73
73
|
* Compares both script file and data file (if present).
|
|
74
74
|
*/
|
|
75
75
|
export declare function hookContentMatches(agentId: AgentId, hookName: string, sourceEntry: HookEntry): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Hooks dir for an agent under an arbitrary home (version home or effective
|
|
78
|
+
* home). An agent whose `hooksDir` is configured absolute under `$HOME` (grok,
|
|
79
|
+
* kimi) must be translated to config-dir-relative first — a raw `path.join`
|
|
80
|
+
* would embed the absolute path as a relative segment and produce a hybrid
|
|
81
|
+
* path that never exists (RUSH-2237).
|
|
82
|
+
*/
|
|
83
|
+
export declare function getHooksDirInHome(agentId: AgentId, home: string): string;
|
|
76
84
|
export declare function listInstalledHooksWithScope(agentId: AgentId, cwd?: string, options?: {
|
|
77
85
|
home?: string;
|
|
78
86
|
}): InstalledHook[];
|
|
@@ -99,7 +107,7 @@ export interface HookWiringIssue {
|
|
|
99
107
|
* group). Real hooks scope by matcher — ask-user-question-guard=AskUserQuestion,
|
|
100
108
|
* user-message-guard=Bash — so wiring is verified per (event, matcher). */
|
|
101
109
|
matcher: string;
|
|
102
|
-
/** The command
|
|
110
|
+
/** The command the harness-native config should reference under `event`. */
|
|
103
111
|
command: string;
|
|
104
112
|
}
|
|
105
113
|
export interface HookWiringReport {
|
|
@@ -109,18 +117,22 @@ export interface HookWiringReport {
|
|
|
109
117
|
settingsPath?: string;
|
|
110
118
|
/** Number of hooks the manifest says should be wired for this version. */
|
|
111
119
|
expected?: number;
|
|
112
|
-
/**
|
|
120
|
+
/** Native hook config does not exist — nothing declared can be wired. */
|
|
113
121
|
settingsMissing?: boolean;
|
|
114
|
-
/**
|
|
122
|
+
/** Native hook config cannot be parsed — wiring can't be verified. */
|
|
115
123
|
settingsUnparseable?: boolean;
|
|
116
124
|
/** Hooks whose file is present/resolvable but that are NOT referenced in the
|
|
117
|
-
* event
|
|
125
|
+
* native event group/entry should carry them in. */
|
|
118
126
|
unwired: HookWiringIssue[];
|
|
127
|
+
/** Expected hooks that ARE referenced in native config (expected − unwired).
|
|
128
|
+
* Empty whenever wiring cannot be verified (unsupported family, missing or
|
|
129
|
+
* unparseable settings). */
|
|
130
|
+
wired: HookWiringIssue[];
|
|
119
131
|
}
|
|
120
132
|
/**
|
|
121
|
-
* Verify that every hook the manifest says should be wired
|
|
122
|
-
*
|
|
123
|
-
*
|
|
133
|
+
* Verify that every hook the manifest says should be wired is actually
|
|
134
|
+
* referenced in that version's harness-native config, not merely present as a
|
|
135
|
+
* file on disk.
|
|
124
136
|
*
|
|
125
137
|
* `agents doctor` compares hook FILES against source (see diffHooks in
|
|
126
138
|
* doctor-diff.ts) but never checks the wiring, so a hook whose script is
|
package/dist/lib/hooks.js
CHANGED
|
@@ -673,6 +673,20 @@ export function hookContentMatches(agentId, hookName, sourceEntry) {
|
|
|
673
673
|
return false;
|
|
674
674
|
}
|
|
675
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Hooks dir for an agent under an arbitrary home (version home or effective
|
|
678
|
+
* home). An agent whose `hooksDir` is configured absolute under `$HOME` (grok,
|
|
679
|
+
* kimi) must be translated to config-dir-relative first — a raw `path.join`
|
|
680
|
+
* would embed the absolute path as a relative segment and produce a hybrid
|
|
681
|
+
* path that never exists (RUSH-2237).
|
|
682
|
+
*/
|
|
683
|
+
export function getHooksDirInHome(agentId, home) {
|
|
684
|
+
const config = AGENTS[agentId];
|
|
685
|
+
const hooksDir = path.isAbsolute(config.hooksDir)
|
|
686
|
+
? path.relative(config.configDir, config.hooksDir)
|
|
687
|
+
: config.hooksDir;
|
|
688
|
+
return path.join(home, agentConfigDirName(agentId), hooksDir);
|
|
689
|
+
}
|
|
676
690
|
export function listInstalledHooksWithScope(agentId, cwd = process.cwd(), options) {
|
|
677
691
|
const agent = AGENTS[agentId];
|
|
678
692
|
if (!agent.supportsHooks) {
|
|
@@ -702,7 +716,7 @@ export function listInstalledHooksWithScope(agentId, cwd = process.cwd(), option
|
|
|
702
716
|
}
|
|
703
717
|
// User-scoped hooks (version-aware when home is provided)
|
|
704
718
|
const home = options?.home || getEffectiveHome(agentId);
|
|
705
|
-
const userDir =
|
|
719
|
+
const userDir = getHooksDirInHome(agentId, home);
|
|
706
720
|
const userHooks = listHookEntriesFromDir(userDir);
|
|
707
721
|
for (const hook of userHooks) {
|
|
708
722
|
addHook(hook, 'user', agentId);
|
|
@@ -740,8 +754,7 @@ export async function installHooks(source, agents, options = {}) {
|
|
|
740
754
|
* Path to the hooks dir of a specific version home (not the active one).
|
|
741
755
|
*/
|
|
742
756
|
export function getVersionHooksDir(agent, version) {
|
|
743
|
-
|
|
744
|
-
return path.join(home, agentConfigDirName(agent), AGENTS[agent].hooksDir);
|
|
757
|
+
return getHooksDirInHome(agent, getVersionHomePath(agent, version));
|
|
745
758
|
}
|
|
746
759
|
/**
|
|
747
760
|
* List hook entries in a specific version home.
|
|
@@ -749,20 +762,20 @@ export function getVersionHooksDir(agent, version) {
|
|
|
749
762
|
export function listHooksInVersionHome(agent, version) {
|
|
750
763
|
return listHookEntriesFromDir(getVersionHooksDir(agent, version));
|
|
751
764
|
}
|
|
752
|
-
// ─── wiring inspection
|
|
765
|
+
// ─── wiring inspection ────────────────────────────────────────────────────────
|
|
753
766
|
/**
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
* map (Gemini/Antigravity settings.json variants, Codex config.toml, the OpenCode
|
|
759
|
-
* plugin, …), so it reports them unsupported rather than risk a false verdict.
|
|
767
|
+
* Native hook-config families understood by this read-only inspector. Claude,
|
|
768
|
+
* Droid, and Muse share settings.json; Grok uses the same grouped event shape
|
|
769
|
+
* in hooks/hooks.json; Kimi stores one hook per [[hooks]] config.toml table.
|
|
770
|
+
* Other harnesses report unsupported rather than risk a false verdict.
|
|
760
771
|
*/
|
|
761
|
-
const SETTINGS_JSON_HOOK_FAMILY = ['claude', 'droid'];
|
|
772
|
+
const SETTINGS_JSON_HOOK_FAMILY = ['claude', 'droid', 'muse'];
|
|
773
|
+
const HOOKS_JSON_HOOK_FAMILY = ['grok'];
|
|
774
|
+
const TOML_ARRAY_HOOK_FAMILY = ['kimi'];
|
|
762
775
|
/**
|
|
763
|
-
* Verify that every hook the manifest says should be wired
|
|
764
|
-
*
|
|
765
|
-
*
|
|
776
|
+
* Verify that every hook the manifest says should be wired is actually
|
|
777
|
+
* referenced in that version's harness-native config, not merely present as a
|
|
778
|
+
* file on disk.
|
|
766
779
|
*
|
|
767
780
|
* `agents doctor` compares hook FILES against source (see diffHooks in
|
|
768
781
|
* doctor-diff.ts) but never checks the wiring, so a hook whose script is
|
|
@@ -774,11 +787,18 @@ const SETTINGS_JSON_HOOK_FAMILY = ['claude', 'droid'];
|
|
|
774
787
|
* resolveHookCommand performs, so it never mutates the version home.
|
|
775
788
|
*/
|
|
776
789
|
export function checkVersionHookWiring(agent, version) {
|
|
777
|
-
if (!AGENTS[agent].supportsHooks ||
|
|
778
|
-
|
|
790
|
+
if (!AGENTS[agent].supportsHooks ||
|
|
791
|
+
(!SETTINGS_JSON_HOOK_FAMILY.includes(agent) &&
|
|
792
|
+
!HOOKS_JSON_HOOK_FAMILY.includes(agent) &&
|
|
793
|
+
!TOML_ARRAY_HOOK_FAMILY.includes(agent))) {
|
|
794
|
+
return { supported: false, unwired: [], wired: [] };
|
|
779
795
|
}
|
|
780
796
|
const versionHome = getVersionHomePath(agent, version);
|
|
781
|
-
const settingsPath =
|
|
797
|
+
const settingsPath = HOOKS_JSON_HOOK_FAMILY.includes(agent)
|
|
798
|
+
? path.join(versionHome, '.grok', 'hooks', 'hooks.json')
|
|
799
|
+
: TOML_ARRAY_HOOK_FAMILY.includes(agent)
|
|
800
|
+
? path.join(versionHome, '.kimi-code', 'config.toml')
|
|
801
|
+
: path.join(versionHome, agentConfigDirName(agent), 'settings.json');
|
|
782
802
|
const localHooksDir = getVersionHooksDir(agent, version);
|
|
783
803
|
// Resolve ONLY to a script that was actually synced for THIS agent+version: the
|
|
784
804
|
// copy in the version home hooks dir, or an absolute subrule-dir path (those are
|
|
@@ -815,11 +835,17 @@ export function checkVersionHookWiring(agent, version) {
|
|
|
815
835
|
const command = expectedCommand(name, hookDef);
|
|
816
836
|
if (!command)
|
|
817
837
|
continue; // script unresolved — a file gap, reported by diffHooks
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
838
|
+
for (const event of hookDef.events) {
|
|
839
|
+
if (HOOKS_JSON_HOOK_FAMILY.includes(agent)) {
|
|
840
|
+
const matcher = GROK_MATCHER_EVENTS.has(event)
|
|
841
|
+
? (GROK_MATCHER_ALIASES[hookDef.matcher || ''] ?? hookDef.matcher ?? '')
|
|
842
|
+
: '';
|
|
843
|
+
expected.push({ name, event, matcher, command });
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
expected.push({ name, event, matcher: hookDef.matcher || '', command });
|
|
847
|
+
}
|
|
848
|
+
}
|
|
823
849
|
}
|
|
824
850
|
if (!fs.existsSync(settingsPath)) {
|
|
825
851
|
return {
|
|
@@ -828,11 +854,15 @@ export function checkVersionHookWiring(agent, version) {
|
|
|
828
854
|
expected: expected.length,
|
|
829
855
|
settingsMissing: expected.length > 0,
|
|
830
856
|
unwired: [],
|
|
857
|
+
wired: [],
|
|
831
858
|
};
|
|
832
859
|
}
|
|
833
860
|
let config;
|
|
834
861
|
try {
|
|
835
|
-
|
|
862
|
+
const raw = fs.readFileSync(settingsPath, 'utf-8');
|
|
863
|
+
config = TOML_ARRAY_HOOK_FAMILY.includes(agent)
|
|
864
|
+
? TOML.parse(raw)
|
|
865
|
+
: JSON.parse(raw);
|
|
836
866
|
}
|
|
837
867
|
catch {
|
|
838
868
|
return {
|
|
@@ -841,6 +871,7 @@ export function checkVersionHookWiring(agent, version) {
|
|
|
841
871
|
expected: expected.length,
|
|
842
872
|
settingsUnparseable: true,
|
|
843
873
|
unwired: [],
|
|
874
|
+
wired: [],
|
|
844
875
|
};
|
|
845
876
|
}
|
|
846
877
|
// Command strings actually referenced, keyed by (event, matcher) — a hook wired
|
|
@@ -848,30 +879,49 @@ export function checkVersionHookWiring(agent, version) {
|
|
|
848
879
|
// not just by event.
|
|
849
880
|
const wiredByGroup = new Map();
|
|
850
881
|
const groupKey = (event, matcher) => `${event}\n${matcher}`;
|
|
851
|
-
|
|
852
|
-
? config.hooks
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
if (!Array.isArray(groups))
|
|
856
|
-
continue;
|
|
857
|
-
for (const group of groups) {
|
|
858
|
-
if (!group || !Array.isArray(group.hooks))
|
|
882
|
+
if (TOML_ARRAY_HOOK_FAMILY.includes(agent)) {
|
|
883
|
+
const hooks = Array.isArray(config.hooks) ? config.hooks : [];
|
|
884
|
+
for (const hook of hooks) {
|
|
885
|
+
if (typeof hook.event !== 'string' || typeof hook.command !== 'string')
|
|
859
886
|
continue;
|
|
860
|
-
const matcher = typeof
|
|
861
|
-
const key = groupKey(event, matcher);
|
|
887
|
+
const matcher = typeof hook.matcher === 'string' ? hook.matcher : '';
|
|
888
|
+
const key = groupKey(hook.event, matcher);
|
|
862
889
|
let cmds = wiredByGroup.get(key);
|
|
863
890
|
if (!cmds) {
|
|
864
891
|
cmds = new Set();
|
|
865
892
|
wiredByGroup.set(key, cmds);
|
|
866
893
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
894
|
+
cmds.add(hook.command);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
const hooks = config.hooks && typeof config.hooks === 'object'
|
|
899
|
+
? config.hooks
|
|
900
|
+
: {};
|
|
901
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
902
|
+
if (!Array.isArray(groups))
|
|
903
|
+
continue;
|
|
904
|
+
for (const group of groups) {
|
|
905
|
+
if (!group || !Array.isArray(group.hooks))
|
|
906
|
+
continue;
|
|
907
|
+
const matcher = typeof group.matcher === 'string' ? group.matcher : '';
|
|
908
|
+
const key = groupKey(event, matcher);
|
|
909
|
+
let cmds = wiredByGroup.get(key);
|
|
910
|
+
if (!cmds) {
|
|
911
|
+
cmds = new Set();
|
|
912
|
+
wiredByGroup.set(key, cmds);
|
|
913
|
+
}
|
|
914
|
+
for (const h of group.hooks) {
|
|
915
|
+
if (h && typeof h.command === 'string')
|
|
916
|
+
cmds.add(h.command);
|
|
917
|
+
}
|
|
870
918
|
}
|
|
871
919
|
}
|
|
872
920
|
}
|
|
873
|
-
const
|
|
874
|
-
|
|
921
|
+
const isWired = (entry) => wiredByGroup.get(groupKey(entry.event, entry.matcher))?.has(entry.command) ?? false;
|
|
922
|
+
const unwired = expected.filter((entry) => !isWired(entry));
|
|
923
|
+
const wired = expected.filter(isWired);
|
|
924
|
+
return { supported: true, settingsPath, expected: expected.length, unwired, wired };
|
|
875
925
|
}
|
|
876
926
|
/**
|
|
877
927
|
* Check if a hook installed in a specific version matches central content.
|
|
@@ -1402,7 +1452,7 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
1402
1452
|
// Scripts are copied into the version home during sync — prefer that stable
|
|
1403
1453
|
// local path so registered commands don't break when source dirs change.
|
|
1404
1454
|
const localHooksDir = !overrideRoots
|
|
1405
|
-
?
|
|
1455
|
+
? getHooksDirInHome(agentId, versionHome)
|
|
1406
1456
|
: null;
|
|
1407
1457
|
const resolveScript = (script) => {
|
|
1408
1458
|
// Subrule-dir hooks declare an already-absolute script path. Use it
|
|
@@ -1453,6 +1503,12 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
1453
1503
|
// registrar targeting `.factory/settings.json` (agentConfigDirName('droid')).
|
|
1454
1504
|
return registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes, agentConfigDirName('droid'));
|
|
1455
1505
|
}
|
|
1506
|
+
if (agentId === 'muse') {
|
|
1507
|
+
// Muse Code: Claude-compatible hooks block in ~/.config/muse/settings.json
|
|
1508
|
+
// (events SessionStart / PreToolUse / …, matcher groups, command hooks).
|
|
1509
|
+
// settings.json MUST carry schema_version: 1 or Muse refuses to start.
|
|
1510
|
+
return registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes, agentConfigDirName('muse'), { schemaVersion: 1 });
|
|
1511
|
+
}
|
|
1456
1512
|
if (agentId === 'codex') {
|
|
1457
1513
|
return registerHooksForCodex(versionHome, manifest, resolveScript, managedPrefixes);
|
|
1458
1514
|
}
|
|
@@ -1675,7 +1731,7 @@ const ANTIGRAVITY_EVENT_MAP = {
|
|
|
1675
1731
|
Stop: 'on_loop_stop',
|
|
1676
1732
|
OnError: 'on_error',
|
|
1677
1733
|
};
|
|
1678
|
-
function registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes, configDirName = '.claude') {
|
|
1734
|
+
function registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes, configDirName = '.claude', options) {
|
|
1679
1735
|
const registered = [];
|
|
1680
1736
|
const errors = [];
|
|
1681
1737
|
const configDir = path.join(versionHome, configDirName);
|
|
@@ -1692,6 +1748,10 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
|
|
|
1692
1748
|
return { registered, errors };
|
|
1693
1749
|
}
|
|
1694
1750
|
}
|
|
1751
|
+
// Muse (and any future agent that pins a settings schema) requires this key.
|
|
1752
|
+
if (options?.schemaVersion !== undefined && config.schema_version === undefined) {
|
|
1753
|
+
config.schema_version = options.schemaVersion;
|
|
1754
|
+
}
|
|
1695
1755
|
if (!config.hooks || typeof config.hooks !== 'object') {
|
|
1696
1756
|
config.hooks = {};
|
|
1697
1757
|
}
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* issued from, so the run appears in that project's session listing like any
|
|
12
12
|
* local run. The `[host/<name>]` label mirrors the cloud path's
|
|
13
13
|
* `[cloud/<status>]` convention.
|
|
14
|
+
*
|
|
15
|
+
* `machine` is the dispatch host, not this box. Both the transcript and the
|
|
16
|
+
* version-isolated harness home live there; omitting it makes the SQLite upsert
|
|
17
|
+
* infer this box from the empty file path and sends recovery to the wrong home.
|
|
14
18
|
*/
|
|
15
19
|
import type { SessionMeta } from '../session/types.js';
|
|
16
20
|
import { type HostTask } from './tasks.js';
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* issued from, so the run appears in that project's session listing like any
|
|
12
12
|
* local run. The `[host/<name>]` label mirrors the cloud path's
|
|
13
13
|
* `[cloud/<status>]` convention.
|
|
14
|
+
*
|
|
15
|
+
* `machine` is the dispatch host, not this box. Both the transcript and the
|
|
16
|
+
* version-isolated harness home live there; omitting it makes the SQLite upsert
|
|
17
|
+
* infer this box from the empty file path and sends recovery to the wrong home.
|
|
14
18
|
*/
|
|
15
19
|
import * as fs from 'fs';
|
|
16
20
|
import { upsertSession } from '../session/db.js';
|
|
@@ -18,6 +22,7 @@ import { isSessionTrackedAgent } from '../session/types.js';
|
|
|
18
22
|
import { localLogPath, updateTask } from './tasks.js';
|
|
19
23
|
import { parseSessionIdMarker } from './session-marker.js';
|
|
20
24
|
import { deriveShortId } from '../session/short-id.js';
|
|
25
|
+
import { normalizeHost } from '../machine-id.js';
|
|
21
26
|
/**
|
|
22
27
|
* Build the SessionMeta for a host-dispatched run. Returns null when the run has
|
|
23
28
|
* no captured session id (nothing stable to key/resume on) or its agent isn't a
|
|
@@ -38,6 +43,7 @@ export function hostSessionMeta(task, ctx) {
|
|
|
38
43
|
// Remote transcript — no local file. Empty file_path is the sentinel the DB
|
|
39
44
|
// stale-filter treats as "always live" (see module doc).
|
|
40
45
|
filePath: '',
|
|
46
|
+
machine: normalizeHost(task.host),
|
|
41
47
|
topic: ctx.prompt.split('\n')[0]?.slice(0, 120) || undefined,
|
|
42
48
|
// The run's `--name` seeds the label (resolves `agents sessions <name>` and
|
|
43
49
|
// `agents hosts logs <name>`); an unnamed host run falls back to the
|
|
@@ -109,6 +115,7 @@ export function registerInteractiveHostSession(ctx) {
|
|
|
109
115
|
timestamp: ctx.createdAt ?? new Date().toISOString(),
|
|
110
116
|
cwd: ctx.cwd,
|
|
111
117
|
filePath: '',
|
|
118
|
+
machine: normalizeHost(ctx.host),
|
|
112
119
|
label: ctx.name || `[host/${ctx.host}]`,
|
|
113
120
|
}, '');
|
|
114
121
|
}
|
package/dist/lib/manifest.d.ts
CHANGED
|
@@ -3,8 +3,18 @@ import type { Manifest } from './types.js';
|
|
|
3
3
|
export declare const MANIFEST_FILENAME = "agents.yaml";
|
|
4
4
|
/** Parse a YAML string into a typed Manifest object. */
|
|
5
5
|
export declare function parseManifest(content: string): Manifest;
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Serialize a Manifest to YAML WITHOUT destroying hand-written comments.
|
|
8
|
+
*
|
|
9
|
+
* Plain `yaml.stringify(manifest)` drops every comment, so `agents mcp add`
|
|
10
|
+
* (and every other writeManifest caller) used to clobber annotations in
|
|
11
|
+
* agents.yaml. Matching `serializeCentral` in state.ts: when existing file
|
|
12
|
+
* text is provided, parse it into a `yaml.Document` (comments + key order
|
|
13
|
+
* preserved) and edit only keys that actually changed. Untouched keys and
|
|
14
|
+
* their comments stay byte-stable. Falls back to plain stringify when there
|
|
15
|
+
* is no existing document yet.
|
|
16
|
+
*/
|
|
17
|
+
export declare function serializeManifest(manifest: Manifest, existingContent?: string | null): string;
|
|
8
18
|
/** Read and parse agents.yaml from a directory. Returns null if the file does not exist. */
|
|
9
19
|
export declare function readManifest(repoPath: string): Manifest | null;
|
|
10
20
|
/** Write a Manifest object to agents.yaml in the given directory. */
|
package/dist/lib/manifest.js
CHANGED
|
@@ -16,9 +16,48 @@ const manifestLockDepth = new Map();
|
|
|
16
16
|
export function parseManifest(content) {
|
|
17
17
|
return yaml.parse(content);
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Serialize a Manifest to YAML WITHOUT destroying hand-written comments.
|
|
21
|
+
*
|
|
22
|
+
* Plain `yaml.stringify(manifest)` drops every comment, so `agents mcp add`
|
|
23
|
+
* (and every other writeManifest caller) used to clobber annotations in
|
|
24
|
+
* agents.yaml. Matching `serializeCentral` in state.ts: when existing file
|
|
25
|
+
* text is provided, parse it into a `yaml.Document` (comments + key order
|
|
26
|
+
* preserved) and edit only keys that actually changed. Untouched keys and
|
|
27
|
+
* their comments stay byte-stable. Falls back to plain stringify when there
|
|
28
|
+
* is no existing document yet.
|
|
29
|
+
*/
|
|
30
|
+
export function serializeManifest(manifest, existingContent) {
|
|
31
|
+
const entries = Object.entries(manifest).filter(([, v]) => v !== undefined);
|
|
32
|
+
const isEmpty = entries.length === 0;
|
|
33
|
+
if (existingContent == null || existingContent.trim() === '') {
|
|
34
|
+
return isEmpty ? '' : yaml.stringify(manifest, { indent: 2 });
|
|
35
|
+
}
|
|
36
|
+
const doc = yaml.parseDocument(existingContent);
|
|
37
|
+
const current = doc.toJSON() ?? {};
|
|
38
|
+
let changed = false;
|
|
39
|
+
for (const [k, v] of entries) {
|
|
40
|
+
if (JSON.stringify(current[k]) !== JSON.stringify(v)) {
|
|
41
|
+
doc.set(k, v);
|
|
42
|
+
changed = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Full-document write: callers do read-modify-write, so keys absent from the
|
|
46
|
+
// new manifest are intentional removals (e.g. clearing beta).
|
|
47
|
+
for (const k of Object.keys(current)) {
|
|
48
|
+
const next = manifest[k];
|
|
49
|
+
if (!(k in manifest) || next === undefined) {
|
|
50
|
+
doc.delete(k);
|
|
51
|
+
changed = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Nothing changed → keep the file byte-identical (comments intact).
|
|
55
|
+
if (!changed)
|
|
56
|
+
return existingContent;
|
|
57
|
+
// Force BLOCK style: an existing flow root (e.g. legacy `{}`) would otherwise
|
|
58
|
+
// make edited nodes render flow. collectionStyle pins the whole doc block
|
|
59
|
+
// while parseDocument still preserves comments + key ordering.
|
|
60
|
+
return isEmpty ? '' : doc.toString({ collectionStyle: 'block' });
|
|
22
61
|
}
|
|
23
62
|
/** Read and parse agents.yaml from a directory. Returns null if the file does not exist. */
|
|
24
63
|
export function readManifest(repoPath) {
|
|
@@ -55,8 +94,24 @@ function withManifestLock(filePath, fn) {
|
|
|
55
94
|
/** Write a Manifest object to agents.yaml in the given directory. */
|
|
56
95
|
export function writeManifest(repoPath, manifest) {
|
|
57
96
|
const manifestPath = safeJoin(repoPath, MANIFEST_FILENAME);
|
|
58
|
-
|
|
59
|
-
|
|
97
|
+
withManifestLock(manifestPath, () => {
|
|
98
|
+
let existing = null;
|
|
99
|
+
try {
|
|
100
|
+
existing = fs.readFileSync(manifestPath, 'utf-8');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
/* first write — no file yet (or empty lock target) */
|
|
104
|
+
}
|
|
105
|
+
// ensureLockTarget may have created an empty file for the lock path.
|
|
106
|
+
if (existing !== null && existing.trim() === '')
|
|
107
|
+
existing = null;
|
|
108
|
+
const content = serializeManifest(manifest, existing);
|
|
109
|
+
// Skip the atomic rewrite when nothing changed so comments stay byte-stable
|
|
110
|
+
// and concurrent readers never see a no-op churn.
|
|
111
|
+
if (existing !== null && content === existing)
|
|
112
|
+
return;
|
|
113
|
+
atomicWriteFileSync(manifestPath, content);
|
|
114
|
+
});
|
|
60
115
|
}
|
|
61
116
|
/** Create a Manifest with sensible defaults for a fresh agents repo. */
|
|
62
117
|
export function createDefaultManifest() {
|
package/dist/lib/mcp.js
CHANGED
|
@@ -595,6 +595,7 @@ function writeMcpConfigSupportsAgent(agentId) {
|
|
|
595
595
|
case 'opencode':
|
|
596
596
|
case 'hermes':
|
|
597
597
|
case 'pi':
|
|
598
|
+
case 'muse':
|
|
598
599
|
return true;
|
|
599
600
|
default:
|
|
600
601
|
return false;
|
|
@@ -797,6 +798,49 @@ export function writeMcpConfig(agentId, configPath, servers, mode = 'overwrite')
|
|
|
797
798
|
fs.writeFileSync(configPath, yaml.stringify(config), 'utf-8');
|
|
798
799
|
break;
|
|
799
800
|
}
|
|
801
|
+
case 'muse': {
|
|
802
|
+
// Muse Code settings.json: requires schema_version: 1; MCP lives under
|
|
803
|
+
// mcp_servers with an explicit transport (stdio | streamable_http).
|
|
804
|
+
// See https://dev.meta.ai/docs/muse-code/extending#mcp
|
|
805
|
+
let config = {};
|
|
806
|
+
if (fs.existsSync(configPath)) {
|
|
807
|
+
try {
|
|
808
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
config = {};
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (config.schema_version === undefined) {
|
|
815
|
+
config.schema_version = 1;
|
|
816
|
+
}
|
|
817
|
+
const mcpServers = mode === 'merge' && config.mcp_servers && typeof config.mcp_servers === 'object' && !Array.isArray(config.mcp_servers)
|
|
818
|
+
? { ...config.mcp_servers }
|
|
819
|
+
: {};
|
|
820
|
+
for (const server of servers) {
|
|
821
|
+
if (server.transport === 'stdio') {
|
|
822
|
+
mcpServers[server.name] = {
|
|
823
|
+
transport: 'stdio',
|
|
824
|
+
command: server.command,
|
|
825
|
+
args: server.args || [],
|
|
826
|
+
...(server.env && Object.keys(server.env).length > 0 ? { env: server.env } : {}),
|
|
827
|
+
enabled: true,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
mcpServers[server.name] = {
|
|
832
|
+
transport: 'streamable_http',
|
|
833
|
+
url: server.url,
|
|
834
|
+
...(server.headers && Object.keys(server.headers).length > 0 ? { headers: server.headers } : {}),
|
|
835
|
+
enabled: true,
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
config.mcp_servers = mcpServers;
|
|
840
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
841
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
800
844
|
}
|
|
801
845
|
}
|
|
802
846
|
/**
|
|
Binary file
|
|
Binary file
|
|
@@ -111,6 +111,54 @@ export declare function menubarPlistNeedsRepoint(opts: {
|
|
|
111
111
|
* upgrade migration won't re-enable it.
|
|
112
112
|
*/
|
|
113
113
|
export declare function disableMenubarService(): void;
|
|
114
|
+
/**
|
|
115
|
+
* Which install is allowed to (re)install the shared helper.
|
|
116
|
+
*
|
|
117
|
+
* The helper lives at ONE path in Application Support, but any number of
|
|
118
|
+
* agents-cli copies can be present on a box and every one of them runs the
|
|
119
|
+
* startup self-heal. The version stamp and the plist's baked `AGENTS_ENTRY` each
|
|
120
|
+
* record whichever copy acted last, so without an ownership rule every copy
|
|
121
|
+
* reads the others' marks as drift and recopies the bundle over them. Recopying
|
|
122
|
+
* replaces the executable under the live helper and kills it; launchd
|
|
123
|
+
* `KeepAlive` restarts it; the next copy repeats it. Measured on one box: a new
|
|
124
|
+
* pid every 5-15s, 578 launches in the helper's log, a status item that never
|
|
125
|
+
* stayed visible, and `agents menubar status` still reporting `running: yes`
|
|
126
|
+
* because a pid always existed (#2109).
|
|
127
|
+
*
|
|
128
|
+
* There is deliberately NO content comparison here. Comparing the shipped helper
|
|
129
|
+
* against the installed one cannot distinguish "real upgrade" from "another
|
|
130
|
+
* install's copy": the helper is rebuilt, re-signed and re-notarized on every
|
|
131
|
+
* release (`menubar/scripts/build.sh` via `release.sh`), so consecutive releases
|
|
132
|
+
* ship byte-different bundles from identical Swift source — 1.22.20/21/22 all
|
|
133
|
+
* have the same 2876288-byte executable and three different sha256s AND three
|
|
134
|
+
* different CDHashes. Any digest gate therefore reports "changed" for exactly
|
|
135
|
+
* the skew case it was meant to exempt.
|
|
136
|
+
*
|
|
137
|
+
* So ownership decides instead: the plist's `AGENTS_ENTRY` names the owner, and
|
|
138
|
+
* only the owner may reinstall. A non-owner takes over only once the recorded
|
|
139
|
+
* owner is gone from disk, which is what makes the rule converge — a dead
|
|
140
|
+
* install cannot hold the helper hostage, and a live one cannot be fought over.
|
|
141
|
+
* A same-install upgrade keeps its entry path, so `npm update` still installs
|
|
142
|
+
* the new helper normally. Pure so the truth table is unit-testable.
|
|
143
|
+
*/
|
|
144
|
+
export declare function mayInstallMenubarHelper(opts: {
|
|
145
|
+
/** `AGENTS_ENTRY` baked into the installed plist — the recorded owner. */
|
|
146
|
+
plistEntry: string | null;
|
|
147
|
+
/** `resolveCliEntry()` for the install now running `agents`. */
|
|
148
|
+
activeEntry: string | null;
|
|
149
|
+
/** Whether `plistEntry` still exists on disk. */
|
|
150
|
+
ownerEntryExists: boolean;
|
|
151
|
+
/** The App Support helper executable is absent — a repair, not a contest. */
|
|
152
|
+
helperExecMissing: boolean;
|
|
153
|
+
/** Installed copy is ad-hoc while the shipped source is Developer ID. */
|
|
154
|
+
needsDevIdHeal: boolean;
|
|
155
|
+
/** ms since the last self-heal reinstall, or null if none is recorded. */
|
|
156
|
+
msSinceLastHeal: number | null;
|
|
157
|
+
/** How long a non-owner waits before it may take over. */
|
|
158
|
+
cooldownMs: number;
|
|
159
|
+
/** This install's OWN shipped bundle is Developer-ID signed (not ad-hoc/dev). */
|
|
160
|
+
sourceIsDeveloperId: boolean;
|
|
161
|
+
}): boolean;
|
|
114
162
|
/**
|
|
115
163
|
* Startup self-heal, run on every darwin CLI invocation (see src/index.ts).
|
|
116
164
|
* No-ops cheaply (a couple of existsSync + a tiny file read) unless work is
|
|
@@ -121,8 +169,10 @@ export declare function disableMenubarService(): void;
|
|
|
121
169
|
*
|
|
122
170
|
* Without the staleness re-enable, `npm update` refreshed the CLI but left the
|
|
123
171
|
* menu bar running the previous release's helper binary on a possibly-stale
|
|
124
|
-
* plist.
|
|
125
|
-
*
|
|
172
|
+
* plist. Everything past the ownership gate is unchanged; the gate is what stops
|
|
173
|
+
* coexisting installs reinstalling over each other forever (#2109). No-ops if:
|
|
174
|
+
* not darwin, the user opted out, or no helper bundle ships. Best-effort — never
|
|
175
|
+
* throws into startup.
|
|
126
176
|
*/
|
|
127
177
|
export declare function installMenubarLaunchAgentOnUpgrade(): void;
|
|
128
178
|
/** One step of `agents menubar setup`, and how it came out. */
|