@phnx-labs/agents-cli 1.15.0 → 1.16.0
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 +78 -39
- package/README.md +6 -6
- package/dist/commands/alias.js +2 -2
- package/dist/commands/browser-picker.d.ts +21 -0
- package/dist/commands/browser-picker.js +114 -0
- package/dist/commands/browser.js +546 -75
- package/dist/commands/commands.js +72 -22
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/fork.js +2 -2
- package/dist/commands/hooks.js +71 -26
- package/dist/commands/mcp.js +81 -39
- package/dist/commands/plugins.js +48 -15
- package/dist/commands/prune.js +23 -1
- package/dist/commands/pull.js +3 -3
- package/dist/commands/repo.js +1 -1
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.js +37 -1
- package/dist/commands/sessions.js +62 -19
- package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
- package/dist/commands/{init.js → setup.js} +22 -21
- package/dist/commands/skills.js +60 -19
- package/dist/commands/subagents.js +41 -13
- package/dist/commands/utils.d.ts +16 -0
- package/dist/commands/utils.js +32 -0
- package/dist/commands/view.js +61 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -20
- package/dist/lib/agents.js +2 -2
- package/dist/lib/auto-pull-worker.js +2 -3
- package/dist/lib/auto-pull.js +2 -2
- package/dist/lib/browser/cdp.d.ts +7 -1
- package/dist/lib/browser/cdp.js +29 -1
- package/dist/lib/browser/chrome.js +5 -2
- package/dist/lib/browser/devices.d.ts +4 -0
- package/dist/lib/browser/devices.js +27 -0
- package/dist/lib/browser/drivers/local.js +9 -4
- package/dist/lib/browser/drivers/ssh.js +9 -2
- package/dist/lib/browser/ipc.js +144 -23
- package/dist/lib/browser/profiles.d.ts +5 -2
- package/dist/lib/browser/profiles.js +77 -37
- package/dist/lib/browser/service.d.ts +81 -13
- package/dist/lib/browser/service.js +738 -131
- package/dist/lib/browser/types.d.ts +81 -3
- package/dist/lib/browser/types.js +16 -0
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/commands.d.ts +1 -0
- package/dist/lib/commands.js +6 -2
- package/dist/lib/daemon.js +2 -3
- package/dist/lib/doctor-diff.js +4 -4
- package/dist/lib/events.js +2 -2
- package/dist/lib/hooks.d.ts +11 -7
- package/dist/lib/hooks.js +125 -49
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +1178 -21
- package/dist/lib/models.js +2 -2
- package/dist/lib/permissions.d.ts +8 -8
- package/dist/lib/permissions.js +8 -8
- package/dist/lib/plugins.d.ts +30 -1
- package/dist/lib/plugins.js +75 -3
- package/dist/lib/pty-server.js +9 -10
- package/dist/lib/resources/hooks.d.ts +5 -1
- package/dist/lib/resources/hooks.js +21 -4
- package/dist/lib/rotate.js +3 -4
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +92 -6
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.js +8 -3
- package/dist/lib/session/discover.js +30 -15
- package/dist/lib/session/team-filter.js +2 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +6 -6
- package/dist/lib/skills.js +6 -2
- package/dist/lib/state.d.ts +86 -14
- package/dist/lib/state.js +150 -23
- package/dist/lib/subagents.d.ts +28 -0
- package/dist/lib/subagents.js +98 -1
- package/dist/lib/sync-manifest.d.ts +1 -1
- package/dist/lib/sync-manifest.js +3 -3
- package/dist/lib/teams/persistence.js +15 -5
- package/dist/lib/teams/registry.js +2 -2
- package/dist/lib/types.d.ts +32 -3
- package/dist/lib/types.js +3 -3
- package/dist/lib/usage.js +2 -2
- package/dist/lib/versions.js +20 -21
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -1
package/dist/lib/models.js
CHANGED
|
@@ -11,8 +11,8 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import { execFileSync } from 'child_process';
|
|
13
13
|
import { getVersionDir } from './versions.js';
|
|
14
|
-
import {
|
|
15
|
-
const CACHE_PATH =
|
|
14
|
+
import { getModelsCachePath } from './state.js';
|
|
15
|
+
const CACHE_PATH = getModelsCachePath();
|
|
16
16
|
/**
|
|
17
17
|
* Bump when the extractor logic changes shape in an incompatible way so cached
|
|
18
18
|
* catalogs from older agents-cli builds are re-extracted.
|
|
@@ -42,26 +42,26 @@ export declare function discoverPermissionGroups(): PermissionGroupInfo[];
|
|
|
42
42
|
*/
|
|
43
43
|
export declare function getTotalPermissionRuleCount(): number;
|
|
44
44
|
/**
|
|
45
|
-
* A permission
|
|
46
|
-
* Lives at ~/.agents/permissions/
|
|
45
|
+
* A permission preset recipe — names a preset and lists which groups it composes.
|
|
46
|
+
* Lives at ~/.agents/permissions/presets/<name>.yaml.
|
|
47
47
|
*/
|
|
48
|
-
export interface
|
|
48
|
+
export interface PermissionPresetRecipe {
|
|
49
49
|
name: string;
|
|
50
50
|
description?: string;
|
|
51
51
|
includes: string[];
|
|
52
52
|
}
|
|
53
53
|
/** Env var that selects which set recipe to apply at sync time. */
|
|
54
|
-
export declare const
|
|
54
|
+
export declare const PERMISSION_PRESET_ENV_VAR = "AGENTS_PERMISSION_PRESET";
|
|
55
55
|
/**
|
|
56
|
-
* Read a permission
|
|
56
|
+
* Read a permission preset recipe by name from ~/.agents/permissions/presets/.
|
|
57
57
|
* Returns null if the recipe file is missing or malformed.
|
|
58
58
|
*/
|
|
59
|
-
export declare function
|
|
59
|
+
export declare function readPermissionPresetRecipe(name: string): PermissionPresetRecipe | null;
|
|
60
60
|
/**
|
|
61
|
-
* Return the active permission
|
|
61
|
+
* Return the active permission preset name from AGENTS_PERMISSION_PRESET env var,
|
|
62
62
|
* or null if unset. Caller decides the default behavior when null.
|
|
63
63
|
*/
|
|
64
|
-
export declare function
|
|
64
|
+
export declare function getActivePermissionPresetName(): string | null;
|
|
65
65
|
/**
|
|
66
66
|
* Build a PermissionSet from selected groups.
|
|
67
67
|
* Concatenates allow/deny rules from each group.
|
package/dist/lib/permissions.js
CHANGED
|
@@ -159,15 +159,15 @@ export function getTotalPermissionRuleCount() {
|
|
|
159
159
|
return groups.reduce((sum, g) => sum + g.ruleCount, 0);
|
|
160
160
|
}
|
|
161
161
|
/** Env var that selects which set recipe to apply at sync time. */
|
|
162
|
-
export const
|
|
162
|
+
export const PERMISSION_PRESET_ENV_VAR = 'AGENTS_PERMISSION_PRESET';
|
|
163
163
|
/**
|
|
164
|
-
* Read a permission
|
|
164
|
+
* Read a permission preset recipe by name from ~/.agents/permissions/presets/.
|
|
165
165
|
* Returns null if the recipe file is missing or malformed.
|
|
166
166
|
*/
|
|
167
|
-
export function
|
|
168
|
-
const
|
|
167
|
+
export function readPermissionPresetRecipe(name) {
|
|
168
|
+
const presetsDir = path.join(getPermissionsDir(), 'presets');
|
|
169
169
|
for (const ext of ['.yaml', '.yml']) {
|
|
170
|
-
const filePath = safeJoin(
|
|
170
|
+
const filePath = safeJoin(presetsDir, name + ext);
|
|
171
171
|
if (!fs.existsSync(filePath))
|
|
172
172
|
continue;
|
|
173
173
|
try {
|
|
@@ -190,11 +190,11 @@ export function readPermissionSetRecipe(name) {
|
|
|
190
190
|
return null;
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
193
|
-
* Return the active permission
|
|
193
|
+
* Return the active permission preset name from AGENTS_PERMISSION_PRESET env var,
|
|
194
194
|
* or null if unset. Caller decides the default behavior when null.
|
|
195
195
|
*/
|
|
196
|
-
export function
|
|
197
|
-
const v = process.env[
|
|
196
|
+
export function getActivePermissionPresetName() {
|
|
197
|
+
const v = process.env[PERMISSION_PRESET_ENV_VAR];
|
|
198
198
|
return v && v.trim() ? v.trim() : null;
|
|
199
199
|
}
|
|
200
200
|
/**
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -73,7 +73,36 @@ export declare function removePluginFromVersion(pluginName: string, pluginRoot:
|
|
|
73
73
|
};
|
|
74
74
|
/**
|
|
75
75
|
* Remove orphaned plugin skill directories from a version home.
|
|
76
|
+
* Soft-deletes to ~/.agents/.trash/plugins/.
|
|
76
77
|
* An orphan is a skill dir with the plugin prefix pattern (name--skill)
|
|
77
78
|
* where the plugin no longer exists in ~/.agents/plugins/.
|
|
78
79
|
*/
|
|
79
|
-
export declare function cleanOrphanedPluginSkills(agent: AgentId, versionHome: string, activePluginNames: Set<string
|
|
80
|
+
export declare function cleanOrphanedPluginSkills(agent: AgentId, versionHome: string, activePluginNames: Set<string>, version?: string): string[];
|
|
81
|
+
export interface VersionPluginDiff {
|
|
82
|
+
agent: AgentId;
|
|
83
|
+
version: string;
|
|
84
|
+
orphans: string[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Compare a version home's plugin skills against discovered plugins.
|
|
88
|
+
* Returns orphan plugin skill names (pattern: pluginName--skillName).
|
|
89
|
+
*/
|
|
90
|
+
export declare function diffVersionPlugins(agent: AgentId, version: string): VersionPluginDiff;
|
|
91
|
+
/**
|
|
92
|
+
* Iterate all (agent, version) pairs that support plugins and are installed.
|
|
93
|
+
*/
|
|
94
|
+
export declare function iterPluginsCapableVersions(filter?: {
|
|
95
|
+
agent?: AgentId;
|
|
96
|
+
version?: string;
|
|
97
|
+
}): Array<{
|
|
98
|
+
agent: AgentId;
|
|
99
|
+
version: string;
|
|
100
|
+
}>;
|
|
101
|
+
/**
|
|
102
|
+
* Remove a single orphan plugin skill from a version home.
|
|
103
|
+
* Soft-deletes to ~/.agents/.trash/plugins/.
|
|
104
|
+
*/
|
|
105
|
+
export declare function removePluginSkillFromVersion(agent: AgentId, version: string, skillName: string): {
|
|
106
|
+
success: boolean;
|
|
107
|
+
error?: string;
|
|
108
|
+
};
|
package/dist/lib/plugins.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
|
-
import { getPluginsDir } from './state.js';
|
|
10
|
+
import { getPluginsDir, getTrashPluginsDir } from './state.js';
|
|
11
|
+
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
11
12
|
import { AGENTS, PLUGINS_CAPABLE_AGENTS } from './agents.js';
|
|
12
13
|
const PLUGIN_MANIFEST_DIR = '.claude-plugin';
|
|
13
14
|
const PLUGIN_MANIFEST_FILE = 'plugin.json';
|
|
@@ -527,10 +528,11 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
|
|
|
527
528
|
}
|
|
528
529
|
/**
|
|
529
530
|
* Remove orphaned plugin skill directories from a version home.
|
|
531
|
+
* Soft-deletes to ~/.agents/.trash/plugins/.
|
|
530
532
|
* An orphan is a skill dir with the plugin prefix pattern (name--skill)
|
|
531
533
|
* where the plugin no longer exists in ~/.agents/plugins/.
|
|
532
534
|
*/
|
|
533
|
-
export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames) {
|
|
535
|
+
export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames, version) {
|
|
534
536
|
const removed = [];
|
|
535
537
|
const skillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
536
538
|
if (!fs.existsSync(skillsDir))
|
|
@@ -546,7 +548,11 @@ export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames)
|
|
|
546
548
|
const pluginName = entry.name.slice(0, dashIdx);
|
|
547
549
|
if (!activePluginNames.has(pluginName)) {
|
|
548
550
|
try {
|
|
549
|
-
|
|
551
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
552
|
+
const trashDir = path.join(getTrashPluginsDir(), agent, version || 'unknown', entry.name);
|
|
553
|
+
const trashDest = path.join(trashDir, stamp);
|
|
554
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
555
|
+
fs.renameSync(path.join(skillsDir, entry.name), trashDest);
|
|
550
556
|
removed.push(entry.name);
|
|
551
557
|
}
|
|
552
558
|
catch {
|
|
@@ -556,3 +562,69 @@ export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames)
|
|
|
556
562
|
}
|
|
557
563
|
return removed;
|
|
558
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Compare a version home's plugin skills against discovered plugins.
|
|
567
|
+
* Returns orphan plugin skill names (pattern: pluginName--skillName).
|
|
568
|
+
*/
|
|
569
|
+
export function diffVersionPlugins(agent, version) {
|
|
570
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
571
|
+
const skillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
572
|
+
const orphans = [];
|
|
573
|
+
if (!fs.existsSync(skillsDir)) {
|
|
574
|
+
return { agent, version, orphans };
|
|
575
|
+
}
|
|
576
|
+
const activePlugins = new Set(discoverPlugins().map(p => p.name));
|
|
577
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
578
|
+
for (const entry of entries) {
|
|
579
|
+
if (!entry.isDirectory())
|
|
580
|
+
continue;
|
|
581
|
+
const dashIdx = entry.name.indexOf('--');
|
|
582
|
+
if (dashIdx === -1)
|
|
583
|
+
continue;
|
|
584
|
+
const pluginName = entry.name.slice(0, dashIdx);
|
|
585
|
+
if (!activePlugins.has(pluginName)) {
|
|
586
|
+
orphans.push(entry.name);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return { agent, version, orphans: orphans.sort() };
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Iterate all (agent, version) pairs that support plugins and are installed.
|
|
593
|
+
*/
|
|
594
|
+
export function iterPluginsCapableVersions(filter) {
|
|
595
|
+
const pairs = [];
|
|
596
|
+
const agents = filter?.agent ? [filter.agent] : PLUGINS_CAPABLE_AGENTS;
|
|
597
|
+
for (const agent of agents) {
|
|
598
|
+
if (!PLUGINS_CAPABLE_AGENTS.includes(agent))
|
|
599
|
+
continue;
|
|
600
|
+
const versions = listInstalledVersions(agent);
|
|
601
|
+
for (const version of versions) {
|
|
602
|
+
if (filter?.version && filter.version !== version)
|
|
603
|
+
continue;
|
|
604
|
+
pairs.push({ agent, version });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return pairs;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Remove a single orphan plugin skill from a version home.
|
|
611
|
+
* Soft-deletes to ~/.agents/.trash/plugins/.
|
|
612
|
+
*/
|
|
613
|
+
export function removePluginSkillFromVersion(agent, version, skillName) {
|
|
614
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
615
|
+
const skillPath = path.join(versionHome, `.${agent}`, 'skills', skillName);
|
|
616
|
+
if (!fs.existsSync(skillPath)) {
|
|
617
|
+
return { success: true };
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
621
|
+
const trashDir = path.join(getTrashPluginsDir(), agent, version, skillName);
|
|
622
|
+
const trashDest = path.join(trashDir, stamp);
|
|
623
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
624
|
+
fs.renameSync(skillPath, trashDest);
|
|
625
|
+
}
|
|
626
|
+
catch (err) {
|
|
627
|
+
return { success: false, error: err.message };
|
|
628
|
+
}
|
|
629
|
+
return { success: true };
|
|
630
|
+
}
|
package/dist/lib/pty-server.js
CHANGED
|
@@ -14,7 +14,7 @@ import * as path from 'path';
|
|
|
14
14
|
import * as crypto from 'crypto';
|
|
15
15
|
import { execFileSync } from 'child_process';
|
|
16
16
|
import { fileURLToPath } from 'url';
|
|
17
|
-
import {
|
|
17
|
+
import { getPtyDir as getPtyDirRoot } from './state.js';
|
|
18
18
|
/**
|
|
19
19
|
* Capture a stable identifier for a process at the moment it was started.
|
|
20
20
|
* Used to defeat PID reuse: a kill(pid, ...) is only safe when the process
|
|
@@ -53,7 +53,6 @@ export function captureProcessStartTime(pid) {
|
|
|
53
53
|
}
|
|
54
54
|
// --- Constants ---
|
|
55
55
|
const SENTINEL = '__AGENTS_PTY_DONE__';
|
|
56
|
-
const PTY_DIR = 'helpers/pty';
|
|
57
56
|
const SOCKET_NAME = 'pty.sock';
|
|
58
57
|
const PID_FILE = 'pty.pid';
|
|
59
58
|
const LOG_FILE = 'logs.jsonl';
|
|
@@ -84,7 +83,7 @@ function buildPtyEnv() {
|
|
|
84
83
|
}
|
|
85
84
|
/** Get the PTY helper directory, creating it if needed. */
|
|
86
85
|
function getPtyDir() {
|
|
87
|
-
const dir =
|
|
86
|
+
const dir = getPtyDirRoot();
|
|
88
87
|
fs.mkdirSync(dir, { recursive: true });
|
|
89
88
|
return dir;
|
|
90
89
|
}
|
|
@@ -178,7 +177,7 @@ export async function runPtyServer() {
|
|
|
178
177
|
}
|
|
179
178
|
catch (err) {
|
|
180
179
|
console.error('node-pty is required for PTY support.');
|
|
181
|
-
console.error('Install: cd ' +
|
|
180
|
+
console.error('Install: cd ' + '~/agents-cli && bun add node-pty');
|
|
182
181
|
process.exit(1);
|
|
183
182
|
}
|
|
184
183
|
try {
|
|
@@ -188,7 +187,7 @@ export async function runPtyServer() {
|
|
|
188
187
|
}
|
|
189
188
|
catch {
|
|
190
189
|
console.error('@xterm/headless is required for PTY support.');
|
|
191
|
-
console.error('Install: cd ' +
|
|
190
|
+
console.error('Install: cd ' + '~/agents-cli && bun add @xterm/headless');
|
|
192
191
|
process.exit(1);
|
|
193
192
|
}
|
|
194
193
|
const sessions = new Map();
|
|
@@ -493,11 +492,11 @@ export async function runPtyServer() {
|
|
|
493
492
|
});
|
|
494
493
|
conn.on('error', () => { });
|
|
495
494
|
});
|
|
496
|
-
// Lock down
|
|
497
|
-
// user with execute on the parent dir could connect to the socket
|
|
498
|
-
// the listen()-to-chmod() window. macOS BSD AF_UNIX semantics make
|
|
499
|
-
// mode advisory only, so the parent dir is the real boundary.
|
|
500
|
-
const agentsDir =
|
|
495
|
+
// Lock down the PTY scratch dir before opening the socket — without this,
|
|
496
|
+
// any local user with execute on the parent dir could connect to the socket
|
|
497
|
+
// during the listen()-to-chmod() window. macOS BSD AF_UNIX semantics make
|
|
498
|
+
// socket mode advisory only, so the parent dir is the real boundary.
|
|
499
|
+
const agentsDir = getPtyDirRoot();
|
|
501
500
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
502
501
|
fs.chmodSync(agentsDir, 0o700);
|
|
503
502
|
// umask covers any inherited group/other bits while listen() is creating
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HooksHandler - ResourceHandler implementation for hooks.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Hook declarations live in:
|
|
5
|
+
* - System: ~/.agents-system/hooks.yaml (npm-shipped defaults)
|
|
6
|
+
* - User: `hooks:` section of ~/.agents/agents.yaml
|
|
7
|
+
* - Project: <project>/.agents/hooks.yaml
|
|
8
|
+
*
|
|
5
9
|
* Resolution: project > user > system (higher layer wins on name conflict).
|
|
6
10
|
* Non-conflicting hooks from all layers are unioned together.
|
|
7
11
|
*/
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HooksHandler - ResourceHandler implementation for hooks.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Hook declarations live in:
|
|
5
|
+
* - System: ~/.agents-system/hooks.yaml (npm-shipped defaults)
|
|
6
|
+
* - User: `hooks:` section of ~/.agents/agents.yaml
|
|
7
|
+
* - Project: <project>/.agents/hooks.yaml
|
|
8
|
+
*
|
|
5
9
|
* Resolution: project > user > system (higher layer wins on name conflict).
|
|
6
10
|
* Non-conflicting hooks from all layers are unioned together.
|
|
7
11
|
*/
|
|
@@ -10,13 +14,20 @@ import * as path from 'path';
|
|
|
10
14
|
import * as yaml from 'yaml';
|
|
11
15
|
import { getSystemAgentsDir, getUserAgentsDir, getProjectAgentsDir, } from '../state.js';
|
|
12
16
|
/**
|
|
13
|
-
* Get the
|
|
17
|
+
* Get the hook manifest path for a layer dir. The user layer reads from
|
|
18
|
+
* agents.yaml (hooks: section) since that's where user hooks now live.
|
|
19
|
+
* Other layers continue to use a standalone hooks.yaml.
|
|
14
20
|
*/
|
|
15
21
|
function getHooksYamlPath(layerDir) {
|
|
22
|
+
if (layerDir === getUserAgentsDir()) {
|
|
23
|
+
return path.join(layerDir, 'agents.yaml');
|
|
24
|
+
}
|
|
16
25
|
return path.join(layerDir, 'hooks.yaml');
|
|
17
26
|
}
|
|
18
27
|
/**
|
|
19
|
-
* Parse hooks
|
|
28
|
+
* Parse hooks for a layer directory.
|
|
29
|
+
* - User layer: read `hooks:` section from agents.yaml.
|
|
30
|
+
* - System / project layer: read top-level map from hooks.yaml.
|
|
20
31
|
* Returns empty object if file doesn't exist or is invalid.
|
|
21
32
|
*/
|
|
22
33
|
function parseHooksYaml(dir) {
|
|
@@ -27,7 +38,13 @@ function parseHooksYaml(dir) {
|
|
|
27
38
|
try {
|
|
28
39
|
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
29
40
|
const parsed = yaml.parse(content);
|
|
30
|
-
|
|
41
|
+
if (!parsed)
|
|
42
|
+
return {};
|
|
43
|
+
if (dir === getUserAgentsDir()) {
|
|
44
|
+
const hooks = parsed.hooks;
|
|
45
|
+
return (hooks && typeof hooks === 'object') ? hooks : {};
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
31
48
|
}
|
|
32
49
|
catch {
|
|
33
50
|
return {};
|
package/dist/lib/rotate.js
CHANGED
|
@@ -8,12 +8,11 @@ import * as fs from 'fs';
|
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import * as yaml from 'yaml';
|
|
10
10
|
import { getAccountInfo } from './agents.js';
|
|
11
|
-
import { readMeta, writeMeta,
|
|
11
|
+
import { readMeta, writeMeta, getHelpersDir, getUserAgentsDir } from './state.js';
|
|
12
12
|
import { listInstalledVersions, getVersionHomePath, resolveVersion } from './versions.js';
|
|
13
13
|
import { getUsageInfoByIdentity, getUsageLookupKey, isClaudeAuthValid, } from './usage.js';
|
|
14
|
-
const ROTATE_DIR = 'helpers/rotate';
|
|
15
14
|
function getRotateDir() {
|
|
16
|
-
const dir = path.join(
|
|
15
|
+
const dir = path.join(getHelpersDir(), 'rotate');
|
|
17
16
|
fs.mkdirSync(dir, { recursive: true });
|
|
18
17
|
return dir;
|
|
19
18
|
}
|
|
@@ -35,7 +34,7 @@ export function normalizeRunStrategy(value) {
|
|
|
35
34
|
/** Read project-local run strategy from the nearest agents.yaml, if present. */
|
|
36
35
|
export function getProjectRunStrategy(agent, startPath) {
|
|
37
36
|
let dir = path.resolve(startPath);
|
|
38
|
-
const userAgentsYaml = path.join(
|
|
37
|
+
const userAgentsYaml = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
39
38
|
while (dir !== path.dirname(dir)) {
|
|
40
39
|
const manifestPath = path.join(dir, 'agents.yaml');
|
|
41
40
|
if (manifestPath !== userAgentsYaml && fs.existsSync(manifestPath)) {
|
|
@@ -8,7 +8,10 @@ export interface ActiveSession {
|
|
|
8
8
|
pid?: number;
|
|
9
9
|
sessionId?: string;
|
|
10
10
|
cwd?: string;
|
|
11
|
+
/** User-given name from /rename command. */
|
|
11
12
|
label?: string;
|
|
13
|
+
/** First meaningful line of the initial prompt (extracted topic). */
|
|
14
|
+
topic?: string;
|
|
12
15
|
sessionFile?: string;
|
|
13
16
|
startedAtMs?: number;
|
|
14
17
|
status: ActiveStatus;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Active-session detection across every context an agent can run in:
|
|
3
3
|
*
|
|
4
4
|
* - `terminal` — agents launched from VS Code / Cursor / Codium via the
|
|
5
|
-
* agents-cli extension. Published to `~/.agents/
|
|
5
|
+
* agents-cli extension. Published to `~/.agents/.cache/terminals/live-terminals.json`
|
|
6
6
|
* with PID + session UUID per entry.
|
|
7
7
|
* - `teams` — agents spawned by `agents teams add`, tracked in
|
|
8
8
|
* `~/.agents/teams/agents/<id>/meta.json` with a PID the manager polls.
|
|
@@ -23,10 +23,12 @@ import { execFile } from 'child_process';
|
|
|
23
23
|
import { promisify } from 'util';
|
|
24
24
|
import { listActiveTasks } from '../cloud/store.js';
|
|
25
25
|
import { AgentManager } from '../teams/agents.js';
|
|
26
|
-
import {
|
|
26
|
+
import { getTerminalsDir } from '../state.js';
|
|
27
|
+
import { buildClaudeLabelMap } from './discover.js';
|
|
28
|
+
import { extractSessionTopic } from './prompt.js';
|
|
27
29
|
const execFileAsync = promisify(execFile);
|
|
28
30
|
const HOME = os.homedir();
|
|
29
|
-
const LIVE_TERMINALS_FILE = path.join(
|
|
31
|
+
const LIVE_TERMINALS_FILE = path.join(getTerminalsDir(), 'live-terminals.json');
|
|
30
32
|
/**
|
|
31
33
|
* A process is classified `running` if its session file was touched in the
|
|
32
34
|
* last 2 minutes. Every Claude/Codex tool-call appends an event, so a
|
|
@@ -80,9 +82,9 @@ function readLiveTerminals() {
|
|
|
80
82
|
}
|
|
81
83
|
return Array.from(merged.values());
|
|
82
84
|
}
|
|
83
|
-
/** Convert an absolute cwd to the Claude-project folder name (slashes → dashes). */
|
|
85
|
+
/** Convert an absolute cwd to the Claude-project folder name (slashes and dots → dashes). */
|
|
84
86
|
function claudeProjectDirName(cwd) {
|
|
85
|
-
return cwd.replace(
|
|
87
|
+
return cwd.replace(/[/.]/g, '-');
|
|
86
88
|
}
|
|
87
89
|
/**
|
|
88
90
|
* Locate the active Claude session file for a process. If we know the session
|
|
@@ -126,6 +128,79 @@ function classifyActivity(sessionFile) {
|
|
|
126
128
|
return 'running';
|
|
127
129
|
}
|
|
128
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Extract the first user message's content from a Claude JSONL file.
|
|
133
|
+
* Reads only the first ~50 lines for speed, since the user message is
|
|
134
|
+
* typically near the top (after system/queue events).
|
|
135
|
+
*/
|
|
136
|
+
function extractClaudeUserText(parsed) {
|
|
137
|
+
const msg = parsed.message;
|
|
138
|
+
if (!msg?.content)
|
|
139
|
+
return undefined;
|
|
140
|
+
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
141
|
+
const texts = [];
|
|
142
|
+
for (const block of content) {
|
|
143
|
+
if (typeof block === 'string')
|
|
144
|
+
texts.push(block);
|
|
145
|
+
else if (block?.type === 'text' && typeof block.text === 'string')
|
|
146
|
+
texts.push(block.text);
|
|
147
|
+
}
|
|
148
|
+
return texts.join('\n').trim() || undefined;
|
|
149
|
+
}
|
|
150
|
+
function quickExtractTopic(sessionFile) {
|
|
151
|
+
let fd;
|
|
152
|
+
try {
|
|
153
|
+
fd = fs.openSync(sessionFile, 'r');
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const chunkSize = 256 * 1024;
|
|
160
|
+
const maxBytes = 2 * 1024 * 1024;
|
|
161
|
+
let buffer = '';
|
|
162
|
+
let totalRead = 0;
|
|
163
|
+
let linesChecked = 0;
|
|
164
|
+
const maxLines = 30;
|
|
165
|
+
while (totalRead < maxBytes && linesChecked < maxLines) {
|
|
166
|
+
const chunk = Buffer.alloc(chunkSize);
|
|
167
|
+
const bytesRead = fs.readSync(fd, chunk, 0, chunkSize, totalRead);
|
|
168
|
+
if (bytesRead === 0)
|
|
169
|
+
break;
|
|
170
|
+
totalRead += bytesRead;
|
|
171
|
+
buffer += chunk.toString('utf8', 0, bytesRead);
|
|
172
|
+
let lineStart = 0;
|
|
173
|
+
let lineEnd;
|
|
174
|
+
while ((lineEnd = buffer.indexOf('\n', lineStart)) !== -1 && linesChecked < maxLines) {
|
|
175
|
+
const line = buffer.slice(lineStart, lineEnd);
|
|
176
|
+
lineStart = lineEnd + 1;
|
|
177
|
+
linesChecked++;
|
|
178
|
+
if (!line.trim())
|
|
179
|
+
continue;
|
|
180
|
+
let parsed;
|
|
181
|
+
try {
|
|
182
|
+
parsed = JSON.parse(line);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (parsed.type === 'user') {
|
|
188
|
+
const text = extractClaudeUserText(parsed);
|
|
189
|
+
if (text) {
|
|
190
|
+
const topic = extractSessionTopic(text);
|
|
191
|
+
if (topic)
|
|
192
|
+
return topic;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
buffer = buffer.slice(lineStart);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
fs.closeSync(fd);
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
129
204
|
/** Live teams teammates. Reuses AgentManager which already polls PIDs via `kill -0`. */
|
|
130
205
|
export async function listTeamsActive() {
|
|
131
206
|
const mgr = new AgentManager();
|
|
@@ -135,6 +210,7 @@ export async function listTeamsActive() {
|
|
|
135
210
|
const sessionFile = a.agentType === 'claude' && a.cwd
|
|
136
211
|
? findClaudeSessionFile(a.cwd, sessionId ?? undefined)
|
|
137
212
|
: undefined;
|
|
213
|
+
const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
|
|
138
214
|
return {
|
|
139
215
|
context: 'teams',
|
|
140
216
|
kind: a.agentType,
|
|
@@ -142,6 +218,7 @@ export async function listTeamsActive() {
|
|
|
142
218
|
sessionId,
|
|
143
219
|
cwd: a.cwd ?? undefined,
|
|
144
220
|
label: a.name ?? undefined,
|
|
221
|
+
topic,
|
|
145
222
|
sessionFile,
|
|
146
223
|
startedAtMs: a.startedAt.getTime(),
|
|
147
224
|
status: classifyActivity(sessionFile),
|
|
@@ -160,10 +237,16 @@ export async function listTerminalsActive() {
|
|
|
160
237
|
const procByPid = new Map();
|
|
161
238
|
for (const r of await readProcessTable())
|
|
162
239
|
procByPid.set(r.pid, r);
|
|
240
|
+
// Build label map from Claude's sessions/*.json for /rename support
|
|
241
|
+
const labelMap = buildClaudeLabelMap();
|
|
163
242
|
return entries.map((t) => {
|
|
164
243
|
const sessionFile = t.kind === 'claude' && t.cwd
|
|
165
244
|
? findClaudeSessionFile(t.cwd, t.sessionId)
|
|
166
245
|
: undefined;
|
|
246
|
+
// Prefer label from live terminal, fall back to Claude's session label
|
|
247
|
+
const label = t.label ?? (t.sessionId ? labelMap.get(t.sessionId) : undefined) ?? undefined;
|
|
248
|
+
// Extract topic from session file (first meaningful user message)
|
|
249
|
+
const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
|
|
167
250
|
return {
|
|
168
251
|
context: 'terminal',
|
|
169
252
|
kind: t.kind,
|
|
@@ -171,7 +254,8 @@ export async function listTerminalsActive() {
|
|
|
171
254
|
pid: t.pid,
|
|
172
255
|
sessionId: t.sessionId,
|
|
173
256
|
cwd: t.cwd ?? undefined,
|
|
174
|
-
label
|
|
257
|
+
label,
|
|
258
|
+
topic,
|
|
175
259
|
sessionFile,
|
|
176
260
|
startedAtMs: t.startedAtMs,
|
|
177
261
|
status: classifyActivity(sessionFile),
|
|
@@ -355,6 +439,7 @@ export async function listUnattributedActive(attributed) {
|
|
|
355
439
|
const { pid, kind } = candidates[i];
|
|
356
440
|
const cwd = cwds[i];
|
|
357
441
|
const sessionFile = kind === 'claude' && cwd ? findClaudeSessionFile(cwd) : undefined;
|
|
442
|
+
const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
|
|
358
443
|
const host = detectHost(pid, procByPid);
|
|
359
444
|
const context = host && UI_HOSTS.has(host) ? 'terminal' : 'headless';
|
|
360
445
|
out.push({
|
|
@@ -363,6 +448,7 @@ export async function listUnattributedActive(attributed) {
|
|
|
363
448
|
host,
|
|
364
449
|
pid,
|
|
365
450
|
cwd,
|
|
451
|
+
topic,
|
|
366
452
|
sessionFile,
|
|
367
453
|
status: classifyActivity(sessionFile),
|
|
368
454
|
});
|
|
@@ -13,10 +13,10 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as os from 'os';
|
|
15
15
|
import * as yaml from 'yaml';
|
|
16
|
-
import {
|
|
16
|
+
import { getCacheDir } from '../state.js';
|
|
17
17
|
const PROXY_BASE = process.env.RUSH_PROXY_BASE ?? 'https://api.prix.dev';
|
|
18
18
|
const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
19
|
-
const CLOUD_CACHE_DIR = path.join(
|
|
19
|
+
const CLOUD_CACHE_DIR = path.join(getCacheDir(), 'cloud-runs');
|
|
20
20
|
function readToken() {
|
|
21
21
|
if (!fs.existsSync(USER_YAML)) {
|
|
22
22
|
throw new Error('Not logged in to Rush. Run `rush login` first.');
|
package/dist/lib/session/db.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import Database from '../sqlite.js';
|
|
12
|
-
import {
|
|
13
|
-
const SESSIONS_DIR =
|
|
14
|
-
const DB_PATH =
|
|
12
|
+
import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
13
|
+
const SESSIONS_DIR = getSessionsDir();
|
|
14
|
+
const DB_PATH = getSessionsDbPath();
|
|
15
15
|
/** Current schema version; bumped when migrations are added. */
|
|
16
16
|
const SCHEMA_VERSION = 5;
|
|
17
17
|
/**
|
|
@@ -151,6 +151,11 @@ export function getDB() {
|
|
|
151
151
|
db.pragma('journal_mode = WAL');
|
|
152
152
|
db.pragma('synchronous = NORMAL');
|
|
153
153
|
db.pragma('temp_store = MEMORY');
|
|
154
|
+
// Wait up to 10s instead of failing immediately on SQLITE_BUSY. Multiple
|
|
155
|
+
// agents (CLIs, indexers, hooks) all open this DB concurrently; without a
|
|
156
|
+
// busy timeout, parallel writers throw "database is locked" the moment one
|
|
157
|
+
// holds the write lock. 10s is well above any realistic transaction here.
|
|
158
|
+
db.pragma('busy_timeout = 10000');
|
|
154
159
|
db.exec(SCHEMA);
|
|
155
160
|
const current = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
156
161
|
const currentVersion = current ? parseInt(current.value, 10) : 0;
|