@phnx-labs/agents-cli 1.20.59 → 1.20.61
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 +17 -1
- package/README.md +9 -6
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +38 -1
- package/dist/commands/routines.js +2 -0
- package/dist/lib/agents.js +92 -4
- package/dist/lib/hosts/dispatch.d.ts +36 -0
- package/dist/lib/hosts/dispatch.js +40 -2
- package/dist/lib/permissions.d.ts +14 -5
- package/dist/lib/permissions.js +139 -28
- package/dist/lib/plugins.d.ts +8 -0
- package/dist/lib/plugins.js +108 -0
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +5 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +23 -0
- package/dist/lib/routines.js +64 -1
- package/dist/lib/runner.d.ts +7 -0
- package/dist/lib/runner.js +34 -6
- package/dist/lib/shims.js +13 -3
- package/dist/lib/skills.js +14 -1
- package/dist/lib/staleness/detectors/permissions.js +28 -2
- package/dist/lib/staleness/detectors/subagents.js +20 -1
- package/dist/lib/staleness/detectors/workflows.js +62 -0
- package/dist/lib/staleness/writers/subagents.js +13 -5
- package/dist/lib/subagents.d.ts +10 -1
- package/dist/lib/subagents.js +102 -14
- package/dist/lib/tmux/session.d.ts +13 -7
- package/dist/lib/tmux/session.js +23 -8
- package/dist/lib/versions.js +84 -2
- package/dist/lib/workflows.d.ts +14 -3
- package/dist/lib/workflows.js +328 -9
- package/package.json +1 -1
package/dist/lib/shims.js
CHANGED
|
@@ -2024,18 +2024,28 @@ export function releaseAdoptedLauncher(agent, overrides) {
|
|
|
2024
2024
|
// records written before this format existed.
|
|
2025
2025
|
const launcher = lines[1] || getPathShadowingExecutable(agent) || original;
|
|
2026
2026
|
const shimReal = canonical(path.join(shimsDir, AGENTS[agent].cliCommand));
|
|
2027
|
+
const shimPath = path.resolve(path.join(shimsDir, AGENTS[agent].cliCommand));
|
|
2027
2028
|
try {
|
|
2028
2029
|
// Only rewrite the launcher if it currently points at our shim (i.e. we own
|
|
2029
2030
|
// it). If the user has since replaced it themselves, leave it alone.
|
|
2030
2031
|
let pointsAtShim = false;
|
|
2031
2032
|
try {
|
|
2032
|
-
|
|
2033
|
-
|
|
2033
|
+
const stat = fs.lstatSync(launcher);
|
|
2034
|
+
if (stat.isSymbolicLink()) {
|
|
2035
|
+
const target = fs.readlinkSync(launcher);
|
|
2036
|
+
const absoluteTarget = path.resolve(path.dirname(launcher), target);
|
|
2037
|
+
pointsAtShim = canonicalOrNull(launcher) === shimReal || absoluteTarget === shimPath;
|
|
2038
|
+
}
|
|
2034
2039
|
}
|
|
2035
2040
|
catch { /* launcher gone — recreate below */ }
|
|
2036
2041
|
if (pointsAtShim || !fs.existsSync(launcher)) {
|
|
2037
2042
|
try {
|
|
2038
|
-
fs.
|
|
2043
|
+
if (fs.lstatSync(launcher).isSymbolicLink()) {
|
|
2044
|
+
fs.unlinkSync(launcher);
|
|
2045
|
+
}
|
|
2046
|
+
else {
|
|
2047
|
+
fs.rmSync(launcher, { force: true });
|
|
2048
|
+
}
|
|
2039
2049
|
}
|
|
2040
2050
|
catch { /* may not exist */ }
|
|
2041
2051
|
fs.symlinkSync(original, launcher);
|
package/dist/lib/skills.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as fs from 'fs';
|
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import * as os from 'os';
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
|
-
import { ensureSkillsDir, agentConfigDirName } from './agents.js';
|
|
13
|
+
import { AGENTS, ensureSkillsDir, agentConfigDirName } from './agents.js';
|
|
14
14
|
import { capableAgents, isCapable } from './capabilities.js';
|
|
15
15
|
import { getUserSkillsDir, getSkillsDir as getSystemSkillsDir, getProjectAgentsDir, getEnabledExtraRepos, getTrashSkillsDir } from './state.js';
|
|
16
16
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
|
|
@@ -432,6 +432,19 @@ function versionSkillMatches(agent, version, skillName) {
|
|
|
432
432
|
*/
|
|
433
433
|
export function diffVersionSkills(agent, version) {
|
|
434
434
|
const available = new Set(listAllSkills());
|
|
435
|
+
// Goose and other native ~/.agents/skills consumers read central storage
|
|
436
|
+
// directly. They intentionally have no per-version copy to diff, so every
|
|
437
|
+
// available central skill is already current for every supported version.
|
|
438
|
+
if (AGENTS[agent].nativeAgentsSkillsDir) {
|
|
439
|
+
return {
|
|
440
|
+
agent,
|
|
441
|
+
version,
|
|
442
|
+
toAdd: [],
|
|
443
|
+
toUpdate: [],
|
|
444
|
+
matched: Array.from(available).sort(),
|
|
445
|
+
orphans: [],
|
|
446
|
+
};
|
|
447
|
+
}
|
|
435
448
|
const installed = new Set(listSkillsInVersionHome(agent, version));
|
|
436
449
|
const toAdd = [];
|
|
437
450
|
const toUpdate = [];
|
|
@@ -110,8 +110,11 @@ function buildGeminiDetector() {
|
|
|
110
110
|
return [];
|
|
111
111
|
try {
|
|
112
112
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
113
|
-
const
|
|
114
|
-
|
|
113
|
+
const core = settings?.tools?.core;
|
|
114
|
+
const exclude = settings?.tools?.exclude;
|
|
115
|
+
const hasCore = Array.isArray(core) && core.length > 0;
|
|
116
|
+
const hasExclude = Array.isArray(exclude) && exclude.length > 0;
|
|
117
|
+
if (hasCore || hasExclude) {
|
|
115
118
|
return discoverPermissionGroups().map(g => g.name);
|
|
116
119
|
}
|
|
117
120
|
}
|
|
@@ -162,6 +165,28 @@ function buildGrokDetector() {
|
|
|
162
165
|
},
|
|
163
166
|
};
|
|
164
167
|
}
|
|
168
|
+
function buildGooseDetector() {
|
|
169
|
+
return {
|
|
170
|
+
kind: 'permissions',
|
|
171
|
+
agent: 'goose',
|
|
172
|
+
list({ versionHome }) {
|
|
173
|
+
const permissionsPath = path.join(versionHome, '.config', 'goose', 'permission.yaml');
|
|
174
|
+
if (!fs.existsSync(permissionsPath))
|
|
175
|
+
return [];
|
|
176
|
+
try {
|
|
177
|
+
const config = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
|
|
178
|
+
const user = config?.user;
|
|
179
|
+
const count = (Array.isArray(user?.always_allow) ? user.always_allow.length : 0) +
|
|
180
|
+
(Array.isArray(user?.ask_before) ? user.ask_before.length : 0) +
|
|
181
|
+
(Array.isArray(user?.never_allow) ? user.never_allow.length : 0);
|
|
182
|
+
if (count > 0)
|
|
183
|
+
return discoverPermissionGroups().map(g => g.name);
|
|
184
|
+
}
|
|
185
|
+
catch { /* parse fail */ }
|
|
186
|
+
return [];
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
165
190
|
function buildKimiDetector() {
|
|
166
191
|
return {
|
|
167
192
|
kind: 'permissions',
|
|
@@ -248,6 +273,7 @@ const handlers = {
|
|
|
248
273
|
gemini: buildGeminiDetector,
|
|
249
274
|
antigravity: buildAntigravityDetector,
|
|
250
275
|
grok: buildGrokDetector,
|
|
276
|
+
goose: buildGooseDetector,
|
|
251
277
|
kimi: buildKimiDetector,
|
|
252
278
|
cursor: buildCursorDetector,
|
|
253
279
|
droid: buildDroidDetector,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Subagents detector. Claude: flat .md files under `<agentDir>/agents/`.
|
|
2
|
+
* Subagents detector. Claude/Gemini/Grok: flat .md files under `<agentDir>/agents/`.
|
|
3
3
|
* Codex: flat .toml files under `<versionHome>/.codex/agents/`.
|
|
4
4
|
* Droid: flat .md files under `<versionHome>/.factory/droids/`.
|
|
5
5
|
* OpenClaw: subdirectories containing AGENTS.md under `<versionHome>/.openclaw/`.
|
|
@@ -29,6 +29,9 @@ function buildClaudeDetector() {
|
|
|
29
29
|
function buildGrokDetector() {
|
|
30
30
|
return buildFlatMdAgentsDetector('grok', '.grok');
|
|
31
31
|
}
|
|
32
|
+
function buildGeminiDetector() {
|
|
33
|
+
return buildFlatMdAgentsDetector('gemini', '.gemini');
|
|
34
|
+
}
|
|
32
35
|
function buildCodexDetector() {
|
|
33
36
|
return {
|
|
34
37
|
kind: 'subagents',
|
|
@@ -128,13 +131,29 @@ function buildOpenCodeDetector() {
|
|
|
128
131
|
},
|
|
129
132
|
};
|
|
130
133
|
}
|
|
134
|
+
function buildAntigravityDetector() {
|
|
135
|
+
return {
|
|
136
|
+
kind: 'subagents',
|
|
137
|
+
agent: 'antigravity',
|
|
138
|
+
list({ versionHome }) {
|
|
139
|
+
const agentsDir = path.join(versionHome, '.gemini', 'config', 'agents');
|
|
140
|
+
if (!fs.existsSync(agentsDir))
|
|
141
|
+
return [];
|
|
142
|
+
return fs.readdirSync(agentsDir, { withFileTypes: true })
|
|
143
|
+
.filter(d => d.isDirectory() && fs.existsSync(path.join(agentsDir, d.name, 'agent.md')))
|
|
144
|
+
.map(d => d.name);
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
131
148
|
const handlers = {
|
|
132
149
|
claude: buildClaudeDetector,
|
|
133
150
|
copilot: buildCopilotDetector,
|
|
151
|
+
gemini: buildGeminiDetector,
|
|
134
152
|
grok: buildGrokDetector,
|
|
135
153
|
codex: buildCodexDetector,
|
|
136
154
|
kimi: buildKimiDetector,
|
|
137
155
|
opencode: buildOpenCodeDetector,
|
|
156
|
+
antigravity: buildAntigravityDetector,
|
|
138
157
|
droid: buildDroidDetector,
|
|
139
158
|
openclaw: buildOpenclawDetector,
|
|
140
159
|
kiro: buildKiroDetector,
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* containing WORKFLOW.md. Mirrors versions.ts:551-558.
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'fs';
|
|
6
|
+
import * as os from 'os';
|
|
6
7
|
import * as path from 'path';
|
|
8
|
+
import * as yaml from 'yaml';
|
|
7
9
|
import { capableAgents } from '../../capabilities.js';
|
|
8
10
|
import { lazyAgentMap } from '../writers/lazy-map.js';
|
|
9
11
|
function buildWorkflowsDetector(agent) {
|
|
@@ -11,6 +13,66 @@ function buildWorkflowsDetector(agent) {
|
|
|
11
13
|
kind: 'workflows',
|
|
12
14
|
agent,
|
|
13
15
|
list({ versionHome }) {
|
|
16
|
+
if (agent === 'kimi') {
|
|
17
|
+
const skillsDir = path.join(versionHome, '.kimi-code', 'skills');
|
|
18
|
+
if (!fs.existsSync(skillsDir))
|
|
19
|
+
return [];
|
|
20
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
21
|
+
.filter(d => d.isDirectory() && fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md')))
|
|
22
|
+
.filter(d => {
|
|
23
|
+
try {
|
|
24
|
+
const skill = fs.readFileSync(path.join(skillsDir, d.name, 'SKILL.md'), 'utf-8');
|
|
25
|
+
const lines = skill.split('\n');
|
|
26
|
+
if (lines[0] !== '---')
|
|
27
|
+
return false;
|
|
28
|
+
const endIndex = lines.slice(1).findIndex(l => l === '---');
|
|
29
|
+
if (endIndex < 0)
|
|
30
|
+
return false;
|
|
31
|
+
const parsed = yaml.parse(lines.slice(1, endIndex + 1).join('\n'));
|
|
32
|
+
return parsed?.type === 'flow' && parsed.agents_workflow === d.name;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.map(d => d.name);
|
|
39
|
+
}
|
|
40
|
+
if (agent === 'antigravity') {
|
|
41
|
+
// Antigravity user workflows are HOME-global and shared across versions,
|
|
42
|
+
// not version-isolated — see workflows.ts:antigravityWorkflowsDir(). agy
|
|
43
|
+
// scans the real ~/.gemini/config/global_workflows/, so the detector reads
|
|
44
|
+
// the same shared dir the writer targets (versionHome is intentionally unused).
|
|
45
|
+
const dir = path.join(process.env.HOME ?? os.homedir(), '.gemini', 'config', 'global_workflows');
|
|
46
|
+
if (!fs.existsSync(dir))
|
|
47
|
+
return [];
|
|
48
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
49
|
+
.filter(d => d.isFile() && d.name.endsWith('.md') && !d.name.startsWith('.'))
|
|
50
|
+
.filter(d => {
|
|
51
|
+
try {
|
|
52
|
+
const content = fs.readFileSync(path.join(dir, d.name), 'utf-8');
|
|
53
|
+
const lines = content.split('\n');
|
|
54
|
+
if (lines[0] !== '---')
|
|
55
|
+
return false;
|
|
56
|
+
const endIndex = lines.slice(1).findIndex(l => l === '---');
|
|
57
|
+
if (endIndex < 0)
|
|
58
|
+
return false;
|
|
59
|
+
const parsed = yaml.parse(lines.slice(1, endIndex + 1).join('\n'));
|
|
60
|
+
return parsed?.agents_workflow === d.name.slice(0, -'.md'.length);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.map(d => d.name.slice(0, -'.md'.length));
|
|
67
|
+
}
|
|
68
|
+
if (agent === 'goose') {
|
|
69
|
+
const recipesDir = path.join(versionHome, '.config', 'goose', 'recipes');
|
|
70
|
+
if (!fs.existsSync(recipesDir))
|
|
71
|
+
return [];
|
|
72
|
+
return fs.readdirSync(recipesDir, { withFileTypes: true })
|
|
73
|
+
.filter(d => d.isFile() && d.name.endsWith('.yaml') && !d.name.startsWith('.'))
|
|
74
|
+
.map(d => d.name.slice(0, -'.yaml'.length));
|
|
75
|
+
}
|
|
14
76
|
const workflowsDir = path.join(versionHome, 'workflows');
|
|
15
77
|
if (!fs.existsSync(workflowsDir))
|
|
16
78
|
return [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Subagents writer. Claude
|
|
3
|
-
* under
|
|
2
|
+
* Subagents writer. Claude/Gemini/Grok flatten each subagent into a single
|
|
3
|
+
* .md file under their native agents directory. Codex writes TOML under
|
|
4
|
+
* `.codex/agents/`.
|
|
4
5
|
* Droid (Factory AI) flattens each into a custom droid .md under
|
|
5
6
|
* `<versionHome>/.factory/droids/`. OpenClaw copies the full subagent
|
|
6
7
|
* directory (with AGENT.md renamed to AGENTS.md) into
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
import * as fs from 'fs';
|
|
14
15
|
import * as path from 'path';
|
|
15
16
|
import { capableAgents } from '../../capabilities.js';
|
|
16
|
-
import { listInstalledSubagents, transformSubagentForClaude, transformSubagentForCodex, transformSubagentForCopilot, writeKimiSubagentFiles, buildKimiSubagentsParentYaml, KIMI_SUBAGENTS_PARENT_FILE, transformSubagentForOpenCode, transformSubagentForDroid, transformSubagentForKiro, syncSubagentToOpenclaw, parseSubagentFrontmatter, } from '../../subagents.js';
|
|
17
|
+
import { listInstalledSubagents, transformSubagentForClaude, transformSubagentForCodex, transformSubagentForCopilot, writeKimiSubagentFiles, buildKimiSubagentsParentYaml, KIMI_SUBAGENTS_PARENT_FILE, transformSubagentForOpenCode, transformSubagentForAntigravity, transformSubagentForDroid, transformSubagentForKiro, syncSubagentToOpenclaw, parseSubagentFrontmatter, } from '../../subagents.js';
|
|
17
18
|
import { safeJoin } from '../../paths.js';
|
|
18
19
|
import { lazyAgentMap } from './lazy-map.js';
|
|
19
20
|
function buildSubagentsWriter(agent) {
|
|
@@ -29,8 +30,9 @@ function buildSubagentsWriter(agent) {
|
|
|
29
30
|
if (!sub)
|
|
30
31
|
continue;
|
|
31
32
|
try {
|
|
32
|
-
if (agent === 'claude' || agent === 'grok') {
|
|
33
|
-
const
|
|
33
|
+
if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
|
|
34
|
+
const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
|
|
35
|
+
const agentsDir = path.join(versionHome, agentsRoot, 'agents');
|
|
34
36
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
35
37
|
fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.md`), transformSubagentForClaude(sub.path));
|
|
36
38
|
synced.push(sub.name);
|
|
@@ -51,6 +53,12 @@ function buildSubagentsWriter(agent) {
|
|
|
51
53
|
fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.md`), transformSubagentForOpenCode(sub.path));
|
|
52
54
|
synced.push(sub.name);
|
|
53
55
|
}
|
|
56
|
+
else if (agent === 'antigravity') {
|
|
57
|
+
const agentDir = safeJoin(path.join(versionHome, '.gemini', 'config', 'agents'), sub.name);
|
|
58
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
59
|
+
fs.writeFileSync(safeJoin(agentDir, 'agent.md'), transformSubagentForAntigravity(sub.path));
|
|
60
|
+
synced.push(sub.name);
|
|
61
|
+
}
|
|
54
62
|
else if (agent === 'droid') {
|
|
55
63
|
const droidsDir = path.join(versionHome, '.factory', 'droids');
|
|
56
64
|
fs.mkdirSync(droidsDir, { recursive: true });
|
package/dist/lib/subagents.d.ts
CHANGED
|
@@ -68,6 +68,15 @@ export declare function transformSubagentForDroid(subagentDir: string): string;
|
|
|
68
68
|
* See GitHub docs for custom agents.
|
|
69
69
|
*/
|
|
70
70
|
export declare const transformSubagentForCopilot: typeof transformSubagentForDroid;
|
|
71
|
+
/**
|
|
72
|
+
* Transform a subagent into Antigravity's custom-agent markdown shape.
|
|
73
|
+
*
|
|
74
|
+
* Antigravity exposes custom agents as Markdown files with YAML frontmatter,
|
|
75
|
+
* close to Gemini CLI subagents. Keep portable frontmatter fields and flatten
|
|
76
|
+
* sibling markdown files into the prompt body like the other markdown-backed
|
|
77
|
+
* agents.
|
|
78
|
+
*/
|
|
79
|
+
export declare function transformSubagentForAntigravity(subagentDir: string): string;
|
|
71
80
|
/**
|
|
72
81
|
* Transform a subagent into an OpenCode agent markdown file.
|
|
73
82
|
*
|
|
@@ -157,7 +166,7 @@ export declare function removeSubagentFromAgent(subagentName: string, agent: Age
|
|
|
157
166
|
export declare function subagentContentMatches(installedDir: string, sourceDir: string): boolean;
|
|
158
167
|
/**
|
|
159
168
|
* List subagents installed to a specific agent's home
|
|
160
|
-
* Claude: scans ~/.
|
|
169
|
+
* Claude/Gemini/Grok: scans ~/.{agent}/agents/{name}.md
|
|
161
170
|
* Kimi: scans ~/.kimi-code/agents/{name}.yaml (+ sibling .system.md)
|
|
162
171
|
* Kiro: scans ~/.kiro/agents/{name}.json
|
|
163
172
|
* OpenClaw: scans ~/.openclaw/{name}/AGENTS.md
|
package/dist/lib/subagents.js
CHANGED
|
@@ -274,6 +274,39 @@ export function transformSubagentForDroid(subagentDir) {
|
|
|
274
274
|
* See GitHub docs for custom agents.
|
|
275
275
|
*/
|
|
276
276
|
export const transformSubagentForCopilot = transformSubagentForDroid;
|
|
277
|
+
/**
|
|
278
|
+
* Transform a subagent into Antigravity's custom-agent markdown shape.
|
|
279
|
+
*
|
|
280
|
+
* Antigravity exposes custom agents as Markdown files with YAML frontmatter,
|
|
281
|
+
* close to Gemini CLI subagents. Keep portable frontmatter fields and flatten
|
|
282
|
+
* sibling markdown files into the prompt body like the other markdown-backed
|
|
283
|
+
* agents.
|
|
284
|
+
*/
|
|
285
|
+
export function transformSubagentForAntigravity(subagentDir) {
|
|
286
|
+
const agentMd = path.join(subagentDir, 'AGENT.md');
|
|
287
|
+
const frontmatter = parseSubagentFrontmatter(agentMd);
|
|
288
|
+
const body = getSubagentBody(agentMd);
|
|
289
|
+
if (!frontmatter) {
|
|
290
|
+
throw new Error(`Invalid AGENT.md in ${subagentDir}`);
|
|
291
|
+
}
|
|
292
|
+
const frontmatterYaml = yaml.stringify({
|
|
293
|
+
name: frontmatter.name,
|
|
294
|
+
description: frontmatter.description,
|
|
295
|
+
kind: 'local',
|
|
296
|
+
...(frontmatter.model && { model: frontmatter.model }),
|
|
297
|
+
}).trim();
|
|
298
|
+
let result = `---\n${frontmatterYaml}\n---\n\n${body}`;
|
|
299
|
+
const files = fs.readdirSync(subagentDir)
|
|
300
|
+
.filter(f => f.endsWith('.md') && f !== 'AGENT.md')
|
|
301
|
+
.sort();
|
|
302
|
+
for (const file of files) {
|
|
303
|
+
const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
|
|
304
|
+
const sectionName = file.replace('.md', '');
|
|
305
|
+
const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
|
|
306
|
+
result += `\n\n## ${title}\n\n${content}`;
|
|
307
|
+
}
|
|
308
|
+
return `${result.trim()}\n`;
|
|
309
|
+
}
|
|
277
310
|
/**
|
|
278
311
|
* Transform a subagent into an OpenCode agent markdown file.
|
|
279
312
|
*
|
|
@@ -489,10 +522,10 @@ export function syncSubagentToOpenclaw(subagentDir, targetDir) {
|
|
|
489
522
|
* Install a subagent to a specific agent's home
|
|
490
523
|
*/
|
|
491
524
|
export function installSubagentToAgent(subagentDir, subagentName, agent, agentHome) {
|
|
492
|
-
if (agent === 'claude' || agent === 'grok') {
|
|
493
|
-
// Claude / Grok: flatten to single .md under
|
|
494
|
-
|
|
495
|
-
const agentsDir = path.join(agentHome,
|
|
525
|
+
if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
|
|
526
|
+
// Claude / Gemini / Grok: flatten to single .md under the native agents dir.
|
|
527
|
+
const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
|
|
528
|
+
const agentsDir = path.join(agentHome, agentsRoot, 'agents');
|
|
496
529
|
if (!fs.existsSync(agentsDir)) {
|
|
497
530
|
fs.mkdirSync(agentsDir, { recursive: true });
|
|
498
531
|
}
|
|
@@ -543,6 +576,19 @@ export function installSubagentToAgent(subagentDir, subagentName, agent, agentHo
|
|
|
543
576
|
return { success: false, error: String(err) };
|
|
544
577
|
}
|
|
545
578
|
}
|
|
579
|
+
else if (agent === 'antigravity') {
|
|
580
|
+
// Antigravity: custom-agent markdown under ~/.gemini/config/agents/<name>/agent.md.
|
|
581
|
+
const agentDir = safeJoin(path.join(agentHome, '.gemini', 'config', 'agents'), subagentName);
|
|
582
|
+
if (!fs.existsSync(agentDir))
|
|
583
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
584
|
+
try {
|
|
585
|
+
fs.writeFileSync(safeJoin(agentDir, 'agent.md'), transformSubagentForAntigravity(subagentDir));
|
|
586
|
+
return { success: true };
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
return { success: false, error: String(err) };
|
|
590
|
+
}
|
|
591
|
+
}
|
|
546
592
|
else if (agent === 'openclaw') {
|
|
547
593
|
// OpenClaw: copy full directory
|
|
548
594
|
const targetDir = safeJoin(path.join(agentHome, '.openclaw'), subagentName);
|
|
@@ -573,8 +619,8 @@ export function installSubagentToAgent(subagentDir, subagentName, agent, agentHo
|
|
|
573
619
|
*/
|
|
574
620
|
export function removeSubagentFromAgent(subagentName, agent, agentHome) {
|
|
575
621
|
try {
|
|
576
|
-
if (agent === 'claude' || agent === 'grok') {
|
|
577
|
-
const agentsRoot = agent === 'grok' ? '.grok' : '.claude';
|
|
622
|
+
if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
|
|
623
|
+
const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
|
|
578
624
|
const targetPath = safeJoin(path.join(agentHome, agentsRoot, 'agents'), `${subagentName}.md`);
|
|
579
625
|
if (fs.existsSync(targetPath)) {
|
|
580
626
|
fs.unlinkSync(targetPath);
|
|
@@ -604,6 +650,12 @@ export function removeSubagentFromAgent(subagentName, agent, agentHome) {
|
|
|
604
650
|
fs.unlinkSync(targetPath);
|
|
605
651
|
return { success: true };
|
|
606
652
|
}
|
|
653
|
+
else if (agent === 'antigravity') {
|
|
654
|
+
const targetDir = safeJoin(path.join(agentHome, '.gemini', 'config', 'agents'), subagentName);
|
|
655
|
+
if (fs.existsSync(targetDir))
|
|
656
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
657
|
+
return { success: true };
|
|
658
|
+
}
|
|
607
659
|
else if (agent === 'openclaw') {
|
|
608
660
|
const targetDir = safeJoin(path.join(agentHome, '.openclaw'), subagentName);
|
|
609
661
|
if (fs.existsSync(targetDir)) {
|
|
@@ -655,16 +707,17 @@ export function subagentContentMatches(installedDir, sourceDir) {
|
|
|
655
707
|
// source of truth.
|
|
656
708
|
/**
|
|
657
709
|
* List subagents installed to a specific agent's home
|
|
658
|
-
* Claude: scans ~/.
|
|
710
|
+
* Claude/Gemini/Grok: scans ~/.{agent}/agents/{name}.md
|
|
659
711
|
* Kimi: scans ~/.kimi-code/agents/{name}.yaml (+ sibling .system.md)
|
|
660
712
|
* Kiro: scans ~/.kiro/agents/{name}.json
|
|
661
713
|
* OpenClaw: scans ~/.openclaw/{name}/AGENTS.md
|
|
662
714
|
*/
|
|
663
715
|
export function listSubagentsForAgent(agentId, home) {
|
|
664
716
|
const subagents = [];
|
|
665
|
-
if (agentId === 'claude' || agentId === 'grok') {
|
|
666
|
-
// Claude / Grok: flat .md files in agents/
|
|
667
|
-
const
|
|
717
|
+
if (agentId === 'claude' || agentId === 'gemini' || agentId === 'grok') {
|
|
718
|
+
// Claude / Gemini / Grok: flat .md files in agents/
|
|
719
|
+
const agentsRoot = agentId === 'grok' ? '.grok' : agentId === 'gemini' ? '.gemini' : '.claude';
|
|
720
|
+
const agentsDir = path.join(home, agentsRoot, 'agents');
|
|
668
721
|
if (!fs.existsSync(agentsDir))
|
|
669
722
|
return subagents;
|
|
670
723
|
for (const file of fs.readdirSync(agentsDir)) {
|
|
@@ -730,6 +783,20 @@ export function listSubagentsForAgent(agentId, home) {
|
|
|
730
783
|
subagents.push({ name, path: filePath, files: [file], frontmatter });
|
|
731
784
|
}
|
|
732
785
|
}
|
|
786
|
+
else if (agentId === 'antigravity') {
|
|
787
|
+
const agentsDir = path.join(home, '.gemini', 'config', 'agents');
|
|
788
|
+
if (!fs.existsSync(agentsDir))
|
|
789
|
+
return subagents;
|
|
790
|
+
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
791
|
+
if (!entry.isDirectory())
|
|
792
|
+
continue;
|
|
793
|
+
const filePath = path.join(agentsDir, entry.name, 'agent.md');
|
|
794
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile())
|
|
795
|
+
continue;
|
|
796
|
+
const frontmatter = parseSubagentFrontmatter(filePath) ?? { name: entry.name, description: '' };
|
|
797
|
+
subagents.push({ name: entry.name, path: filePath, files: ['agent.md'], frontmatter });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
733
800
|
else if (agentId === 'copilot') {
|
|
734
801
|
// Copilot: flat `<name>.agent.md` files under ~/.copilot/agents/
|
|
735
802
|
const agentsDir = path.join(home, '.copilot', 'agents');
|
|
@@ -840,8 +907,9 @@ export function diffVersionSubagents(agent, version) {
|
|
|
840
907
|
}
|
|
841
908
|
}
|
|
842
909
|
// Check what's installed
|
|
843
|
-
if (agent === 'claude' || agent === 'grok') {
|
|
844
|
-
const
|
|
910
|
+
if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
|
|
911
|
+
const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
|
|
912
|
+
const agentsDir = path.join(versionHome, agentsRoot, 'agents');
|
|
845
913
|
if (fs.existsSync(agentsDir)) {
|
|
846
914
|
for (const file of fs.readdirSync(agentsDir)) {
|
|
847
915
|
if (!file.endsWith('.md'))
|
|
@@ -878,6 +946,19 @@ export function diffVersionSubagents(agent, version) {
|
|
|
878
946
|
}
|
|
879
947
|
}
|
|
880
948
|
}
|
|
949
|
+
else if (agent === 'antigravity') {
|
|
950
|
+
const agentsDir = path.join(versionHome, '.gemini', 'config', 'agents');
|
|
951
|
+
if (fs.existsSync(agentsDir)) {
|
|
952
|
+
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
953
|
+
if (!entry.isDirectory())
|
|
954
|
+
continue;
|
|
955
|
+
if (!fs.existsSync(path.join(agentsDir, entry.name, 'agent.md')))
|
|
956
|
+
continue;
|
|
957
|
+
if (!discovered.has(entry.name))
|
|
958
|
+
orphans.push(entry.name);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
881
962
|
else if (agent === 'openclaw') {
|
|
882
963
|
const openclawDir = path.join(versionHome, '.openclaw');
|
|
883
964
|
if (fs.existsSync(openclawDir)) {
|
|
@@ -932,8 +1013,8 @@ export function removeSubagentFromVersion(agent, version, subagentName) {
|
|
|
932
1013
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
933
1014
|
const trashDir = path.join(getTrashSubagentsDir(), agent, version, subagentName);
|
|
934
1015
|
try {
|
|
935
|
-
if (agent === 'claude' || agent === 'grok') {
|
|
936
|
-
const agentsRoot = agent === 'grok' ? '.grok' : '.claude';
|
|
1016
|
+
if (agent === 'claude' || agent === 'gemini' || agent === 'grok') {
|
|
1017
|
+
const agentsRoot = agent === 'grok' ? '.grok' : agent === 'gemini' ? '.gemini' : '.claude';
|
|
937
1018
|
const targetPath = path.join(versionHome, agentsRoot, 'agents', `${subagentName}.md`);
|
|
938
1019
|
if (fs.existsSync(targetPath)) {
|
|
939
1020
|
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
@@ -961,6 +1042,13 @@ export function removeSubagentFromVersion(agent, version, subagentName) {
|
|
|
961
1042
|
fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
|
|
962
1043
|
}
|
|
963
1044
|
}
|
|
1045
|
+
else if (agent === 'antigravity') {
|
|
1046
|
+
const targetDir = path.join(versionHome, '.gemini', 'config', 'agents', subagentName);
|
|
1047
|
+
if (fs.existsSync(targetDir)) {
|
|
1048
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
1049
|
+
fs.renameSync(targetDir, path.join(trashDir, stamp));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
964
1052
|
else if (agent === 'copilot') {
|
|
965
1053
|
const targetPath = path.join(versionHome, '.copilot', 'agents', `${subagentName}.agent.md`);
|
|
966
1054
|
if (fs.existsSync(targetPath)) {
|
|
@@ -146,19 +146,25 @@ export declare function setSessionHook(name: string, hook: string, command: stri
|
|
|
146
146
|
* in the tmux server instead of launching a second tmux client against
|
|
147
147
|
* the same socket from inside the hook. That self-client could race the
|
|
148
148
|
* server under load and leave the dead split behind.
|
|
149
|
+
* v5 — `run-shell -b -C "kill-pane -t #{hook_pane}"` runs the targeted kill-pane
|
|
150
|
+
* in the background inside the server command queue. The synchronous
|
|
151
|
+
* variant could stall the hook on a loaded CI runner, letting the dead
|
|
152
|
+
* split survive until the test's wait timeout expired (flake in CI shard
|
|
153
|
+
* 3: pane-guarded pane-died hook / user split exit).
|
|
149
154
|
*/
|
|
150
|
-
export declare const AGENT_HOOK_SCHEMA =
|
|
155
|
+
export declare const AGENT_HOOK_SCHEMA = 5;
|
|
151
156
|
/**
|
|
152
157
|
* The guarded `pane-died` hook. Detach the client ONLY when the agent pane dies
|
|
153
158
|
* (so the blocking attach in runInTmux returns and the exit status can be read);
|
|
154
159
|
* a user split's death runs the else-branch, closing just that split. The
|
|
155
|
-
* else-branch goes through `run-shell -C` with an explicit `-t #{hook_pane}`
|
|
160
|
+
* else-branch goes through `run-shell -b -C` with an explicit `-t #{hook_pane}`
|
|
156
161
|
* target: tmux format-expands the command at fire time and executes it inside
|
|
157
|
-
* the server command queue, so the event pane is always the
|
|
158
|
-
* launching a second tmux client against the same socket
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
+
* the server command queue in the background, so the event pane is always the
|
|
163
|
+
* one killed without launching a second tmux client against the same socket and
|
|
164
|
+
* without stalling the hook on a loaded runner. A bare `kill-pane` relied on
|
|
165
|
+
* the hook context supplying a "current pane", while an external self-client
|
|
166
|
+
* could race the server under load. Single source of truth: both the
|
|
167
|
+
* spawn-wrap (exec.ts) and the daemon reconcile build the hook here, so the
|
|
162
168
|
* two can never drift.
|
|
163
169
|
*/
|
|
164
170
|
export declare function agentPaneDiedHook(sessionName: string, agentPane: string): string;
|
package/dist/lib/tmux/session.js
CHANGED
|
@@ -93,6 +93,15 @@ export async function createSession(opts) {
|
|
|
93
93
|
const res = await runTmux({ socket, args, env: opts.env });
|
|
94
94
|
// Only the new-session command in the `;`-chained invocation emits output.
|
|
95
95
|
const pane = /^%\d+$/.test(res.stdout.trim()) ? res.stdout.trim() : undefined;
|
|
96
|
+
// Keep the agent pane around after its process exits (so runInTmux can read
|
|
97
|
+
// the exit status and capture the final error), but do NOT keep that behavior
|
|
98
|
+
// for user-created splits. Apply remain-on-exit to the agent pane only, then
|
|
99
|
+
// revert the global default so future splits close automatically when their
|
|
100
|
+
// command finishes. The global-on above protects a fast-exiting agent during
|
|
101
|
+
// the brief window before the pane option is stamped.
|
|
102
|
+
if (pane) {
|
|
103
|
+
await runTmux({ socket, args: ['set-option', '-pt', pane, 'remain-on-exit', 'on', ';', 'set-option', '-g', 'remain-on-exit', 'off'], throwOnError: false }).catch(() => { });
|
|
104
|
+
}
|
|
96
105
|
const meta = {
|
|
97
106
|
name: opts.name,
|
|
98
107
|
socket,
|
|
@@ -294,25 +303,31 @@ export async function setSessionHook(name, hook, command, socket) {
|
|
|
294
303
|
* in the tmux server instead of launching a second tmux client against
|
|
295
304
|
* the same socket from inside the hook. That self-client could race the
|
|
296
305
|
* server under load and leave the dead split behind.
|
|
306
|
+
* v5 — `run-shell -b -C "kill-pane -t #{hook_pane}"` runs the targeted kill-pane
|
|
307
|
+
* in the background inside the server command queue. The synchronous
|
|
308
|
+
* variant could stall the hook on a loaded CI runner, letting the dead
|
|
309
|
+
* split survive until the test's wait timeout expired (flake in CI shard
|
|
310
|
+
* 3: pane-guarded pane-died hook / user split exit).
|
|
297
311
|
*/
|
|
298
|
-
export const AGENT_HOOK_SCHEMA =
|
|
312
|
+
export const AGENT_HOOK_SCHEMA = 5;
|
|
299
313
|
/** Per-session tmux user-option that records which AGENT_HOOK_SCHEMA a session's hook is at. */
|
|
300
314
|
const HOOK_SCHEMA_OPTION = '@ag_hook_schema';
|
|
301
315
|
/**
|
|
302
316
|
* The guarded `pane-died` hook. Detach the client ONLY when the agent pane dies
|
|
303
317
|
* (so the blocking attach in runInTmux returns and the exit status can be read);
|
|
304
318
|
* a user split's death runs the else-branch, closing just that split. The
|
|
305
|
-
* else-branch goes through `run-shell -C` with an explicit `-t #{hook_pane}`
|
|
319
|
+
* else-branch goes through `run-shell -b -C` with an explicit `-t #{hook_pane}`
|
|
306
320
|
* target: tmux format-expands the command at fire time and executes it inside
|
|
307
|
-
* the server command queue, so the event pane is always the
|
|
308
|
-
* launching a second tmux client against the same socket
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
321
|
+
* the server command queue in the background, so the event pane is always the
|
|
322
|
+
* one killed without launching a second tmux client against the same socket and
|
|
323
|
+
* without stalling the hook on a loaded runner. A bare `kill-pane` relied on
|
|
324
|
+
* the hook context supplying a "current pane", while an external self-client
|
|
325
|
+
* could race the server under load. Single source of truth: both the
|
|
326
|
+
* spawn-wrap (exec.ts) and the daemon reconcile build the hook here, so the
|
|
312
327
|
* two can never drift.
|
|
313
328
|
*/
|
|
314
329
|
export function agentPaneDiedHook(sessionName, agentPane) {
|
|
315
|
-
return `if -F '#{==:#{hook_pane},${agentPane}}' 'detach-client -s =${sessionName}' 'run-shell -C "kill-pane -t #{hook_pane}"'`;
|
|
330
|
+
return `if -F '#{==:#{hook_pane},${agentPane}}' 'detach-client -s =${sessionName}' 'run-shell -b -C "kill-pane -t #{hook_pane}"'`;
|
|
316
331
|
}
|
|
317
332
|
/** Stamp a session's hook-schema marker to the current version. */
|
|
318
333
|
export async function markSessionHookSchema(name, socket) {
|