@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.
- package/CHANGELOG.md +13 -1
- package/README.md +8 -5
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +38 -1
- package/dist/lib/agents.js +6 -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 +16 -0
- package/dist/lib/routines.js +46 -1
- 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 +33 -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/versions.js +12 -2
- package/dist/lib/workflows.d.ts +5 -3
- package/dist/lib/workflows.js +246 -9
- package/package.json +1 -1
package/dist/lib/plugins.js
CHANGED
|
@@ -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. */
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -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. */
|
package/dist/lib/routines.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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
|
|
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
|