@phnx-labs/agents-cli 1.20.59 → 1.20.60

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.
@@ -21,6 +21,7 @@ import { shouldInstallCommandAsSkill, installCommandSkillToVersion } from './com
21
21
  import { copyPluginToMarketplace, syncMarketplaceManifest, registerMarketplace, unregisterMarketplace, addPluginToSettings, removePluginFromSettings, removePluginFromMarketplace, registerDroidInstalledPlugin, unregisterDroidInstalledPlugin, isDroidPluginInstalled, registerCopilotInstalledPlugin, unregisterCopilotInstalledPlugin, marketplaceIsEmpty, removeEmptyMarketplaceDir, isInstalledInMarketplace, marketplaceRoot, discoverMarketplaces, marketplaceNameFor, MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, SYSTEM_MARKETPLACE_NAME, } from './plugin-marketplace.js';
22
22
  const PLUGIN_MANIFEST_DIR = '.claude-plugin';
23
23
  const PLUGIN_MANIFEST_FILE = 'plugin.json';
24
+ const GEMINI_EXTENSION_MANIFEST_FILE = 'gemini-extension.json';
24
25
  const USER_CONFIG_FILE = '.user-config.json';
25
26
  const SOURCE_FILE = '.source';
26
27
  export const PLUGIN_EXEC_SURFACE_LABELS = {
@@ -502,6 +503,27 @@ export function syncPluginToVersion(plugin, agent, versionHome, options = {}) {
502
503
  result.skills.push(plugin.name);
503
504
  return result;
504
505
  }
506
+ // Gemini CLI loads extensions from $HOME/.gemini/extensions/<name>/.
507
+ // Copy the plugin bundle as an extension and synthesize gemini-extension.json.
508
+ if (agent === 'gemini') {
509
+ const enablePlugin = options.allowExecSurfaces === true || !hasPluginExecSurfaces(inspectPluginCapabilities(plugin.root));
510
+ if (!enablePlugin) {
511
+ return result;
512
+ }
513
+ const ok = installGeminiPlugin(plugin, versionHome);
514
+ result.success = ok;
515
+ if (ok) {
516
+ result.skills = plugin.skills.map(s => `${plugin.name}:${s}`);
517
+ result.commands = plugin.commands.map(c => `${plugin.name}:${c}`);
518
+ result.agentDefs = plugin.agentDefs.map(a => `${plugin.name}:${a}`);
519
+ result.bin = plugin.bin;
520
+ result.hooks = plugin.hooks;
521
+ result.mcp = plugin.hasMcp;
522
+ result.settings = plugin.hasSettings;
523
+ result.permissions = pluginHasPermissions(plugin);
524
+ }
525
+ return result;
526
+ }
505
527
  // Goose loads Open Plugins from $HOME/.agents/plugins/<name>/ (same layout as
506
528
  // agents-cli's source tree). Under the shim HOME is the version home.
507
529
  if (agent === 'goose') {
@@ -901,6 +923,82 @@ export function removeOpenCodePlugin(pluginName, versionHome) {
901
923
  }
902
924
  return removed;
903
925
  }
926
+ // ─── Gemini extensions ───────────────────────────────────────────────────────
927
+ /**
928
+ * Gemini CLI extensions live under `$HOME/.gemini/extensions/<name>/` and
929
+ * require a `gemini-extension.json` manifest at the extension root.
930
+ */
931
+ export function geminiExtensionsDir(versionHome) {
932
+ return path.join(versionHome, '.gemini', 'extensions');
933
+ }
934
+ function readPluginMcpConfigForGemini(pluginRoot) {
935
+ const mcpPath = path.join(pluginRoot, '.mcp.json');
936
+ if (!fs.existsSync(mcpPath))
937
+ return undefined;
938
+ try {
939
+ const parsed = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
940
+ if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object')
941
+ return undefined;
942
+ return rewriteGeminiExtensionVars(parsed.mcpServers);
943
+ }
944
+ catch {
945
+ return undefined;
946
+ }
947
+ }
948
+ function rewriteGeminiExtensionVars(value) {
949
+ if (typeof value === 'string') {
950
+ return value
951
+ .replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, '${extensionPath}')
952
+ .replace(/\$\{CLAUDE_PLUGIN_DATA\}/g, '${extensionPath}/.data');
953
+ }
954
+ if (Array.isArray(value))
955
+ return value.map(rewriteGeminiExtensionVars);
956
+ if (value && typeof value === 'object') {
957
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, rewriteGeminiExtensionVars(item)]));
958
+ }
959
+ return value;
960
+ }
961
+ function writeGeminiExtensionManifest(plugin, destRoot) {
962
+ const manifest = {
963
+ name: plugin.manifest.name,
964
+ version: plugin.manifest.version,
965
+ description: plugin.manifest.description,
966
+ };
967
+ const mcpServers = readPluginMcpConfigForGemini(destRoot);
968
+ if (mcpServers && Object.keys(mcpServers).length > 0) {
969
+ manifest.mcpServers = mcpServers;
970
+ }
971
+ fs.writeFileSync(path.join(destRoot, GEMINI_EXTENSION_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
972
+ }
973
+ export function installGeminiPlugin(plugin, versionHome) {
974
+ const destRoot = path.join(geminiExtensionsDir(versionHome), plugin.name);
975
+ try {
976
+ if (fs.existsSync(destRoot)) {
977
+ fs.rmSync(destRoot, { recursive: true, force: true });
978
+ }
979
+ fs.cpSync(plugin.root, destRoot, { recursive: true });
980
+ const userConfig = loadUserConfig(plugin.name);
981
+ if (Object.keys(userConfig).length > 0) {
982
+ expandUserConfigInDir(destRoot, userConfig);
983
+ }
984
+ writeGeminiExtensionManifest(plugin, destRoot);
985
+ fs.writeFileSync(path.join(destRoot, '.agents-cli-managed'), `plugin=${plugin.name}\n`, 'utf-8');
986
+ return true;
987
+ }
988
+ catch {
989
+ return false;
990
+ }
991
+ }
992
+ export function isGeminiPluginInstalled(pluginName, versionHome) {
993
+ return fs.existsSync(path.join(geminiExtensionsDir(versionHome), pluginName, GEMINI_EXTENSION_MANIFEST_FILE));
994
+ }
995
+ export function removeGeminiPlugin(pluginName, versionHome) {
996
+ const destRoot = path.join(geminiExtensionsDir(versionHome), pluginName);
997
+ if (!fs.existsSync(destRoot))
998
+ return false;
999
+ fs.rmSync(destRoot, { recursive: true, force: true });
1000
+ return true;
1001
+ }
904
1002
  // ─── Goose plugins (Open Plugins under .agents/plugins/) ─────────────────────
905
1003
  /**
906
1004
  * Goose auto-discovers Open Plugins at `$HOME/.agents/plugins/<name>/`.
@@ -949,6 +1047,9 @@ export function isPluginSynced(plugin, agent, versionHome) {
949
1047
  if (agent === 'opencode') {
950
1048
  return isOpenCodePluginInstalled(plugin.name, versionHome);
951
1049
  }
1050
+ if (agent === 'gemini') {
1051
+ return isGeminiPluginInstalled(plugin.name, versionHome);
1052
+ }
952
1053
  if (agent === 'goose') {
953
1054
  return isGoosePluginInstalled(plugin.name, versionHome);
954
1055
  }
@@ -986,6 +1087,13 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
986
1087
  }
987
1088
  return result;
988
1089
  }
1090
+ // Gemini: remove extension directory from ~/.gemini/extensions/.
1091
+ if (agent === 'gemini') {
1092
+ if (removeGeminiPlugin(pluginName, versionHome)) {
1093
+ result.skills.push(pluginName);
1094
+ }
1095
+ return result;
1096
+ }
989
1097
  // Goose: remove Open Plugin directory from versionHome/.agents/plugins/.
990
1098
  if (agent === 'goose') {
991
1099
  if (removeGoosePlugin(pluginName, versionHome)) {
@@ -4,7 +4,7 @@
4
4
  * Permissions are stored as YAML files in permissions/ directories at each layer.
5
5
  * Resolution: project > user > system (higher layer wins on name conflict).
6
6
  * Unlike other resources, permissions merge into agent-specific config files
7
- * (Claude: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
7
+ * (Claude/Gemini: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
8
8
  */
9
9
  import type { ResourceHandler } from './types.js';
10
10
  import type { PermissionSet } from '../types.js';
@@ -4,7 +4,7 @@
4
4
  * Permissions are stored as YAML files in permissions/ directories at each layer.
5
5
  * Resolution: project > user > system (higher layer wins on name conflict).
6
6
  * Unlike other resources, permissions merge into agent-specific config files
7
- * (Claude: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
7
+ * (Claude/Gemini: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
8
8
  */
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
@@ -65,6 +65,8 @@ function getAgentConfigPath(agent, versionHome) {
65
65
  return path.join(versionHome, '.claude', 'settings.json');
66
66
  case 'codex':
67
67
  return path.join(versionHome, '.codex', 'config.toml');
68
+ case 'gemini':
69
+ return path.join(versionHome, '.gemini', 'settings.json');
68
70
  case 'opencode':
69
71
  return path.join(versionHome, '.config', 'opencode', 'opencode.jsonc');
70
72
  case 'kimi':
@@ -73,6 +75,8 @@ function getAgentConfigPath(agent, versionHome) {
73
75
  return path.join(versionHome, '.factory', 'settings.json');
74
76
  case 'kiro':
75
77
  return path.join(versionHome, '.kiro', 'settings', 'permissions.yaml');
78
+ case 'goose':
79
+ return path.join(versionHome, '.config', 'goose', 'permission.yaml');
76
80
  default:
77
81
  return null;
78
82
  }
@@ -5,7 +5,7 @@
5
5
  * - Union: All resources from all layers are combined
6
6
  * - Override on name conflict: Higher layer wins (project > user > system)
7
7
  */
8
- export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'kiro' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'forge';
8
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'forge';
9
9
  export type Layer = 'system' | 'user' | 'project';
10
10
  export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow' | 'memory';
11
11
  /** A resolved resource with its origin layer. */
@@ -138,6 +138,22 @@ export declare function readJob(name: string, cwd?: string): JobConfig | null;
138
138
  * name, the write fails explicitly so we never choose or drop a sibling.
139
139
  */
140
140
  export declare function writeJob(config: JobConfig): void;
141
+ /**
142
+ * Serialize a job config, preserving the on-disk formatting of an existing file.
143
+ *
144
+ * A full `yaml.stringify(config)` re-emits the whole document — restyling every
145
+ * scalar (unquoting `schedule`, re-wrapping the folded `prompt` block, reordering
146
+ * keys). When a routine is only being toggled (pause/resume) or re-pinned
147
+ * (`devices --set`), that rewrites the entire file, leaving the git-backed
148
+ * `~/.agents` tree perpetually dirty so `agents repo pull` refuses to sync across
149
+ * the fleet. To keep the diff to the field that actually changed, we edit the
150
+ * existing document in place and only re-render touched nodes; untouched nodes
151
+ * (notably the large `prompt` block) keep their byte-for-byte formatting.
152
+ *
153
+ * `existingText` is the current file contents, or null for a new file. New,
154
+ * unparseable, and non-mapping documents fall back to canonical `yaml.stringify`.
155
+ */
156
+ export declare function serializeJob(output: Record<string, unknown>, existingText: string | null): string;
141
157
  /** Delete a job config file by name. Returns true if the file existed. */
142
158
  export declare function deleteJob(name: string): boolean;
143
159
  /** Enable or disable a job by name. */
@@ -187,7 +187,52 @@ export function writeJob(config) {
187
187
  const devArr = output.devices;
188
188
  if (!devArr || devArr.length === 0)
189
189
  delete output.devices;
190
- atomicWriteFileSync(filePath, yaml.stringify(output));
190
+ let existingText = null;
191
+ if (ymlExists || yamlExists) {
192
+ try {
193
+ existingText = fs.readFileSync(filePath, 'utf-8');
194
+ }
195
+ catch {
196
+ existingText = null;
197
+ }
198
+ }
199
+ atomicWriteFileSync(filePath, serializeJob(output, existingText));
200
+ }
201
+ /**
202
+ * Serialize a job config, preserving the on-disk formatting of an existing file.
203
+ *
204
+ * A full `yaml.stringify(config)` re-emits the whole document — restyling every
205
+ * scalar (unquoting `schedule`, re-wrapping the folded `prompt` block, reordering
206
+ * keys). When a routine is only being toggled (pause/resume) or re-pinned
207
+ * (`devices --set`), that rewrites the entire file, leaving the git-backed
208
+ * `~/.agents` tree perpetually dirty so `agents repo pull` refuses to sync across
209
+ * the fleet. To keep the diff to the field that actually changed, we edit the
210
+ * existing document in place and only re-render touched nodes; untouched nodes
211
+ * (notably the large `prompt` block) keep their byte-for-byte formatting.
212
+ *
213
+ * `existingText` is the current file contents, or null for a new file. New,
214
+ * unparseable, and non-mapping documents fall back to canonical `yaml.stringify`.
215
+ */
216
+ export function serializeJob(output, existingText) {
217
+ if (existingText == null)
218
+ return yaml.stringify(output);
219
+ const doc = yaml.parseDocument(existingText);
220
+ if (doc.errors.length > 0 || !yaml.isMap(doc.contents))
221
+ return yaml.stringify(output);
222
+ const existing = (doc.toJS() ?? {});
223
+ // Update or add only the keys that actually changed; leave the rest untouched
224
+ // so their original formatting is preserved.
225
+ for (const [key, value] of Object.entries(output)) {
226
+ if (JSON.stringify(existing[key]) !== JSON.stringify(value))
227
+ doc.set(key, value);
228
+ }
229
+ // Drop keys that no longer belong (e.g. an omitted default, or `devices`
230
+ // cleared back to fleet-wide).
231
+ for (const key of Object.keys(existing)) {
232
+ if (!(key in output))
233
+ doc.delete(key);
234
+ }
235
+ return doc.toString();
191
236
  }
192
237
  /** Delete a job config file by name. Returns true if the file existed. */
193
238
  export function deleteJob(name) {
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
- pointsAtShim = fs.lstatSync(launcher).isSymbolicLink()
2033
- && canonical(launcher) === shimReal;
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.rmSync(launcher);
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);
@@ -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 allowed = settings?.tools?.allowed;
114
- if (Array.isArray(allowed) && allowed.length > 0) {
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,
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
+ import * as yaml from 'yaml';
7
8
  import { capableAgents } from '../../capabilities.js';
8
9
  import { lazyAgentMap } from '../writers/lazy-map.js';
9
10
  function buildWorkflowsDetector(agent) {
@@ -11,6 +12,38 @@ function buildWorkflowsDetector(agent) {
11
12
  kind: 'workflows',
12
13
  agent,
13
14
  list({ versionHome }) {
15
+ if (agent === 'kimi') {
16
+ const skillsDir = path.join(versionHome, '.kimi-code', 'skills');
17
+ if (!fs.existsSync(skillsDir))
18
+ return [];
19
+ return fs.readdirSync(skillsDir, { withFileTypes: true })
20
+ .filter(d => d.isDirectory() && fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md')))
21
+ .filter(d => {
22
+ try {
23
+ const skill = fs.readFileSync(path.join(skillsDir, d.name, 'SKILL.md'), 'utf-8');
24
+ const lines = skill.split('\n');
25
+ if (lines[0] !== '---')
26
+ return false;
27
+ const endIndex = lines.slice(1).findIndex(l => l === '---');
28
+ if (endIndex < 0)
29
+ return false;
30
+ const parsed = yaml.parse(lines.slice(1, endIndex + 1).join('\n'));
31
+ return parsed?.type === 'flow' && parsed.agents_workflow === d.name;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ })
37
+ .map(d => d.name);
38
+ }
39
+ if (agent === 'goose') {
40
+ const recipesDir = path.join(versionHome, '.config', 'goose', 'recipes');
41
+ if (!fs.existsSync(recipesDir))
42
+ return [];
43
+ return fs.readdirSync(recipesDir, { withFileTypes: true })
44
+ .filter(d => d.isFile() && d.name.endsWith('.yaml') && !d.name.startsWith('.'))
45
+ .map(d => d.name.slice(0, -'.yaml'.length));
46
+ }
14
47
  const workflowsDir = path.join(versionHome, 'workflows');
15
48
  if (!fs.existsSync(workflowsDir))
16
49
  return [];
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Subagents writer. Claude flattens each subagent into a single .md file
3
- * under `<agentDir>/agents/`. Codex writes TOML under `.codex/agents/`.
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 agentsDir = path.join(versionHome, agent === 'grok' ? '.grok' : '.claude', 'agents');
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 });
@@ -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 ~/.claude/agents/{name}.md
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