@michaelhartmayer/agentctl 1.0.2 → 1.0.3

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/dist/index.js CHANGED
@@ -21,10 +21,12 @@ const resolve_1 = require("./resolve");
21
21
  const child_process_1 = require("child_process");
22
22
  const chalk_1 = __importDefault(require("chalk"));
23
23
  const program = new commander_1.Command();
24
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
25
+ const pkg = require('../package.json');
24
26
  program
25
27
  .name('agentctl')
26
28
  .description('Agent Controller CLI - Unified control plane for humans and agents')
27
- .version('1.0.0')
29
+ .version(pkg.version)
28
30
  .allowUnknownOption()
29
31
  .helpOption(false) // Disable default help to allow pass-through
30
32
  .argument('[command...]', 'Command to run')
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@michaelhartmayer/agentctl",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "description": "Agent Controller - A unified interface for humans and AI agents",
7
+ "version": "1.0.3",
8
+ "main": "dist/index.js",
9
+ "bin": {
10
+ "agentctl": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "skills",
15
+ "scripts",
16
+ "agentctl.cmd",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "lint": "eslint src tests",
24
+ "register:path": "node scripts/register-path.js",
25
+ "unregister:path": "node scripts/unregister-path.js",
26
+ "release": "standard-version",
27
+ "prepublishOnly": "npm run build && npm run test",
28
+ "prepare": "husky"
29
+ },
30
+ "keywords": [
31
+ "cli",
32
+ "agent",
33
+ "ai",
34
+ "automation",
35
+ "framework"
36
+ ],
37
+ "author": "Michael Hartmayer",
38
+ "license": "MIT",
39
+ "dependencies": {
40
+ "chalk": "^4.1.2",
41
+ "commander": "^14.0.3",
42
+ "fs-extra": "^11.3.3"
43
+ },
44
+ "devDependencies": {
45
+ "@commitlint/cli": "^20.4.1",
46
+ "@commitlint/config-conventional": "^20.4.1",
47
+ "@types/fs-extra": "^11.0.4",
48
+ "@types/node": "^25.2.3",
49
+ "@typescript-eslint/eslint-plugin": "^8.55.0",
50
+ "@typescript-eslint/parser": "^8.55.0",
51
+ "@vitest/coverage-v8": "^4.0.18",
52
+ "eslint": "^8.57.1",
53
+ "eslint-plugin-eslint-comments": "^3.2.0",
54
+ "husky": "^9.1.7",
55
+ "standard-version": "^9.5.0",
56
+ "ts-node": "^10.9.2",
57
+ "typescript": "^5.9.3",
58
+ "vitest": "^4.0.18"
59
+ }
60
+ }
@@ -0,0 +1,316 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.scaffold = scaffold;
7
+ exports.alias = alias;
8
+ exports.group = group;
9
+ exports.pushGlobal = pushGlobal;
10
+ exports.pullLocal = pullLocal;
11
+ exports.installSkill = installSkill;
12
+ exports.rm = rm;
13
+ exports.mv = mv;
14
+ exports.inspect = inspect;
15
+ exports.list = list;
16
+ const fs_extra_1 = __importDefault(require("fs-extra"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const resolve_1 = require("./resolve");
19
+ const fs_utils_1 = require("./fs-utils");
20
+ const manifest_1 = require("./manifest");
21
+ async function scaffold(args, options = {}) {
22
+ const { targetDir, name, isWin } = await prepareCommand(args, options);
23
+ const scriptName = isWin ? 'command.cmd' : 'command.sh';
24
+ const scriptPath = path_1.default.join(targetDir, scriptName);
25
+ const scriptContent = isWin
26
+ ? '@echo off\r\nREM Add your command logic here\r\necho Not implemented'
27
+ : '#!/usr/bin/env bash\n# Add your command logic here\necho "Not implemented"';
28
+ await fs_extra_1.default.writeFile(scriptPath, scriptContent);
29
+ if (!isWin)
30
+ await fs_extra_1.default.chmod(scriptPath, 0o755);
31
+ const manifest = {
32
+ name,
33
+ description: '',
34
+ type: 'scaffold',
35
+ run: `./${scriptName}`,
36
+ };
37
+ await fs_extra_1.default.writeJson(path_1.default.join(targetDir, 'manifest.json'), manifest, { spaces: 2 });
38
+ console.log(`Scaffolded command: ${args.join(' ')}`);
39
+ }
40
+ async function alias(args, target, options = {}) {
41
+ const { targetDir, name } = await prepareCommand(args, options);
42
+ const manifest = {
43
+ name,
44
+ description: '',
45
+ type: 'alias',
46
+ run: target,
47
+ };
48
+ await fs_extra_1.default.writeJson(path_1.default.join(targetDir, 'manifest.json'), manifest, { spaces: 2 });
49
+ console.log(`Aliased command: ${args.join(' ')} -> ${target}`);
50
+ }
51
+ async function group(args, options = {}) {
52
+ const { targetDir, name } = await prepareCommand(args, options);
53
+ const manifest = {
54
+ name,
55
+ description: '',
56
+ type: 'group',
57
+ };
58
+ await fs_extra_1.default.writeJson(path_1.default.join(targetDir, 'manifest.json'), manifest, { spaces: 2 });
59
+ console.log(`Created group: ${args.join(' ')}`);
60
+ }
61
+ async function pushGlobal(args, options = {}) {
62
+ const cwd = options.cwd || process.cwd();
63
+ const localRoot = (0, fs_utils_1.findLocalRoot)(cwd);
64
+ if (!localRoot)
65
+ throw new Error('Not in a local context');
66
+ const globalRoot = options.globalDir || (0, fs_utils_1.getGlobalRoot)();
67
+ const localAgentctl = path_1.default.join(localRoot, '.agentctl');
68
+ const cmdPathStr = args.join(path_1.default.sep);
69
+ const srcDir = path_1.default.join(localAgentctl, cmdPathStr);
70
+ if (!await fs_extra_1.default.pathExists(srcDir)) {
71
+ throw new Error(`Local command ${args.join(' ')} not found`);
72
+ }
73
+ const destDir = path_1.default.join(globalRoot, cmdPathStr);
74
+ if (await fs_extra_1.default.pathExists(destDir)) {
75
+ throw new Error(`Global command ${args.join(' ')} already exists`);
76
+ }
77
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(destDir));
78
+ if (options.move) {
79
+ await fs_extra_1.default.move(srcDir, destDir);
80
+ console.log(`Moved ${args.join(' ')} to global scope`);
81
+ }
82
+ else {
83
+ await fs_extra_1.default.copy(srcDir, destDir);
84
+ console.log(`Copied ${args.join(' ')} to global scope`);
85
+ }
86
+ }
87
+ async function pullLocal(args, options = {}) {
88
+ const cwd = options.cwd || process.cwd();
89
+ const localRoot = (0, fs_utils_1.findLocalRoot)(cwd);
90
+ if (!localRoot)
91
+ throw new Error('Not in a local context');
92
+ const globalRoot = options.globalDir || (0, fs_utils_1.getGlobalRoot)();
93
+ const cmdPathStr = args.join(path_1.default.sep);
94
+ const srcDir = path_1.default.join(globalRoot, cmdPathStr);
95
+ if (!await fs_extra_1.default.pathExists(srcDir)) {
96
+ throw new Error(`Global command ${args.join(' ')} not found`);
97
+ }
98
+ const localAgentctl = path_1.default.join(localRoot, '.agentctl');
99
+ const destDir = path_1.default.join(localAgentctl, cmdPathStr);
100
+ if (await fs_extra_1.default.pathExists(destDir)) {
101
+ throw new Error(`Local command ${args.join(' ')} already exists`);
102
+ }
103
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(destDir));
104
+ if (options.move) {
105
+ await fs_extra_1.default.move(srcDir, destDir);
106
+ console.log(`Moved ${args.join(' ')} to local scope`);
107
+ }
108
+ else {
109
+ await fs_extra_1.default.copy(srcDir, destDir);
110
+ console.log(`Copied ${args.join(' ')} to local scope`);
111
+ }
112
+ }
113
+ const skills_1 = require("./skills");
114
+ async function installSkill(agent, options = {}) {
115
+ const cwd = options.cwd || process.cwd();
116
+ if (!skills_1.SUPPORTED_AGENTS.includes(agent)) {
117
+ throw new Error(`Agent '${agent}' not supported. Supported agents: ${skills_1.SUPPORTED_AGENTS.join(', ')}`);
118
+ }
119
+ let targetDir;
120
+ if (agent === 'cursor') {
121
+ targetDir = path_1.default.join(cwd, '.cursor', 'skills');
122
+ }
123
+ else if (agent === 'antigravity') {
124
+ if (options.global) {
125
+ const globalRoot = options.antigravityGlobalDir || (0, fs_utils_1.getAntigravityGlobalRoot)();
126
+ targetDir = path_1.default.join(globalRoot, 'skills', 'agentctl');
127
+ }
128
+ else {
129
+ targetDir = path_1.default.join(cwd, '.agent', 'skills', 'agentctl');
130
+ }
131
+ }
132
+ else if (agent === 'agentsmd') {
133
+ targetDir = path_1.default.join(cwd, '.agents', 'skills', 'agentctl');
134
+ }
135
+ else if (agent === 'gemini') {
136
+ if (options.global) {
137
+ const globalRoot = options.geminiGlobalDir || path_1.default.join(process.env.HOME || process.env.USERPROFILE, '.gemini');
138
+ targetDir = path_1.default.join(globalRoot, 'skills', 'agentctl');
139
+ }
140
+ else {
141
+ targetDir = path_1.default.join(cwd, '.gemini', 'skills', 'agentctl');
142
+ }
143
+ }
144
+ else {
145
+ throw new Error(`Agent logic for '${agent}' not implemented.`);
146
+ }
147
+ const p = await (0, skills_1.copySkill)(targetDir, agent);
148
+ console.log(`Installed skill for ${agent} at ${p}`);
149
+ }
150
+ async function rm(args, options = {}) {
151
+ const resolved = await (0, resolve_1.resolveCommand)(args, options);
152
+ if (!resolved) {
153
+ throw new Error(`Command ${args.join(' ')} not found${options.global ? ' in global scope' : ''}`);
154
+ }
155
+ const targetDir = path_1.default.dirname(resolved.manifestPath);
156
+ await fs_extra_1.default.remove(targetDir);
157
+ console.log(`Removed ${resolved.scope} command: ${args.join(' ')}`);
158
+ }
159
+ async function mv(srcArgs, destArgs, options = {}) {
160
+ const resolved = await (0, resolve_1.resolveCommand)(srcArgs, options);
161
+ if (!resolved) {
162
+ throw new Error(`Command ${srcArgs.join(' ')} not found`);
163
+ }
164
+ const srcDir = path_1.default.dirname(resolved.manifestPath);
165
+ const rootDir = resolved.scope === 'local'
166
+ ? (0, fs_utils_1.findLocalRoot)(options.cwd || process.cwd())
167
+ : (options.globalDir || (0, fs_utils_1.getGlobalRoot)());
168
+ if (!rootDir)
169
+ throw new Error('Cannot determine root for move');
170
+ const agentctlDir = resolved.scope === 'local' ? path_1.default.join(rootDir, '.agentctl') : rootDir;
171
+ // For global, rootDir IS the agentctl dir (config dir). Local has .agentctl subdir.
172
+ const destPathStr = destArgs.join(path_1.default.sep);
173
+ const destDir = path_1.default.join(agentctlDir, destPathStr);
174
+ if (await fs_extra_1.default.pathExists(destDir)) {
175
+ throw new Error(`Destination ${destArgs.join(' ')} already exists`);
176
+ }
177
+ // Check parent validity (nesting under capped)
178
+ let current = path_1.default.dirname(destDir);
179
+ while (current.length >= agentctlDir.length && !isSamePath(current, path_1.default.dirname(agentctlDir))) {
180
+ if (await isCapped(current)) {
181
+ const relPath = path_1.default.relative(agentctlDir, current); // relative to base
182
+ throw new Error(`Cannot nest command under capped command: ${relPath}`);
183
+ }
184
+ current = path_1.default.dirname(current);
185
+ }
186
+ await fs_extra_1.default.move(srcDir, destDir);
187
+ // Update manifest name
188
+ const manifestPath = path_1.default.join(destDir, 'manifest.json');
189
+ if (await fs_extra_1.default.pathExists(manifestPath)) {
190
+ const manifest = await fs_extra_1.default.readJson(manifestPath);
191
+ manifest.name = destArgs[destArgs.length - 1];
192
+ await fs_extra_1.default.writeJson(manifestPath, manifest, { spaces: 2 });
193
+ }
194
+ console.log(`Moved ${srcArgs.join(' ')} to ${destArgs.join(' ')}`);
195
+ }
196
+ async function inspect(args, options = {}) {
197
+ const resolved = await (0, resolve_1.resolveCommand)(args, options);
198
+ if (!resolved) {
199
+ return null;
200
+ }
201
+ return {
202
+ manifest: resolved.manifest,
203
+ resolvedPath: resolved.manifestPath,
204
+ scope: resolved.scope
205
+ };
206
+ }
207
+ async function list(options = {}) {
208
+ const cwd = options.cwd || process.cwd();
209
+ const localRoot = (0, fs_utils_1.findLocalRoot)(cwd);
210
+ const globalRoot = options.globalDir || (0, fs_utils_1.getGlobalRoot)();
211
+ const commands = new Map();
212
+ async function walk(dir, prefix, scope) {
213
+ if (!await fs_extra_1.default.pathExists(dir))
214
+ return;
215
+ const files = await fs_extra_1.default.readdir(dir);
216
+ for (const file of files) {
217
+ const filePath = path_1.default.join(dir, file);
218
+ let stats;
219
+ try {
220
+ stats = await fs_extra_1.default.stat(filePath);
221
+ }
222
+ catch {
223
+ continue;
224
+ }
225
+ if (!stats.isDirectory())
226
+ continue;
227
+ const cmdPathParts = [...prefix, file];
228
+ const cmdPath = cmdPathParts.join(' ');
229
+ let manifest = null;
230
+ const mPath = path_1.default.join(filePath, 'manifest.json');
231
+ if (await fs_extra_1.default.pathExists(mPath)) {
232
+ manifest = await (0, manifest_1.readManifest)(mPath);
233
+ }
234
+ let type = 'group';
235
+ if (manifest) {
236
+ if ((0, manifest_1.isCappedManifest)(manifest)) {
237
+ type = manifest.type || 'scaffold';
238
+ }
239
+ else if (manifest.type) {
240
+ type = manifest.type;
241
+ }
242
+ }
243
+ const item = {
244
+ path: cmdPath,
245
+ type,
246
+ scope,
247
+ description: manifest?.description || ''
248
+ };
249
+ if (!commands.has(cmdPath)) {
250
+ commands.set(cmdPath, item);
251
+ const effectiveManifest = manifest || { name: file, type: 'group' };
252
+ if (!(0, manifest_1.isCappedManifest)(effectiveManifest)) {
253
+ await walk(filePath, cmdPathParts, scope);
254
+ }
255
+ }
256
+ else {
257
+ const existing = commands.get(cmdPath);
258
+ if (existing && existing.scope === 'local') {
259
+ if (existing.type === 'group' && type === 'group') {
260
+ await walk(filePath, cmdPathParts, scope);
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ if (localRoot) {
267
+ await walk(path_1.default.join(localRoot, '.agentctl'), [], 'local');
268
+ }
269
+ await walk(globalRoot, [], 'global');
270
+ return Array.from(commands.values());
271
+ }
272
+ // Helpers
273
+ async function prepareCommand(args, options = {}) {
274
+ const cwd = options.cwd || process.cwd();
275
+ const rootDir = cwd;
276
+ const agentctlDir = path_1.default.join(rootDir, '.agentctl');
277
+ if (args.length === 0)
278
+ throw new Error('No command path provided');
279
+ const cmdPath = args.join(path_1.default.sep);
280
+ const targetDir = path_1.default.join(agentctlDir, cmdPath);
281
+ if (await fs_extra_1.default.pathExists(targetDir)) {
282
+ throw new Error(`Command ${args.join(' ')} already exists`);
283
+ }
284
+ let current = path_1.default.dirname(targetDir);
285
+ while (current.length >= agentctlDir.length && !isSamePath(current, path_1.default.dirname(agentctlDir))) {
286
+ if (await isCapped(current)) {
287
+ const relPath = path_1.default.relative(agentctlDir, current); // This uses cwd for resolution if agentctlDir is cwd resolved
288
+ // Need to verify relative works predictably
289
+ // agentctlDir comes from path.join(cwd, '.agentctl').
290
+ throw new Error(`Cannot nest command under capped command: ${relPath}`);
291
+ }
292
+ current = path_1.default.dirname(current);
293
+ }
294
+ await fs_extra_1.default.ensureDir(targetDir);
295
+ return {
296
+ targetDir,
297
+ name: args[args.length - 1],
298
+ isWin: process.platform === 'win32'
299
+ };
300
+ }
301
+ async function isCapped(dir) {
302
+ const manifestPath = path_1.default.join(dir, 'manifest.json');
303
+ if (await fs_extra_1.default.pathExists(manifestPath)) {
304
+ try {
305
+ const m = await fs_extra_1.default.readJson(manifestPath);
306
+ return (0, manifest_1.isCappedManifest)(m);
307
+ }
308
+ catch {
309
+ return false;
310
+ }
311
+ }
312
+ return false;
313
+ }
314
+ function isSamePath(p1, p2) {
315
+ return path_1.default.relative(p1, p2) === '';
316
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getGlobalRoot = getGlobalRoot;
7
+ exports.getAntigravityGlobalRoot = getAntigravityGlobalRoot;
8
+ exports.findLocalRoot = findLocalRoot;
9
+ const path_1 = __importDefault(require("path"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const fs_extra_1 = __importDefault(require("fs-extra"));
12
+ function getGlobalRoot() {
13
+ if (process.platform === 'win32') {
14
+ return path_1.default.join(process.env.APPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Roaming'), 'agentctl');
15
+ }
16
+ return path_1.default.join(os_1.default.homedir(), '.config', 'agentctl');
17
+ }
18
+ function getAntigravityGlobalRoot() {
19
+ return path_1.default.join(os_1.default.homedir(), '.gemini', 'antigravity');
20
+ }
21
+ function findLocalRoot(cwd = process.cwd()) {
22
+ let current = path_1.default.resolve(cwd);
23
+ const root = path_1.default.parse(current).root;
24
+ // Safety break and root check
25
+ // Using for(;;) to avoid no-constant-condition
26
+ for (;;) {
27
+ if (fs_extra_1.default.existsSync(path_1.default.join(current, '.agentctl'))) {
28
+ return current;
29
+ }
30
+ if (current === root) {
31
+ return null;
32
+ }
33
+ current = path_1.default.dirname(current);
34
+ }
35
+ }
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const path_1 = __importDefault(require("path"));
9
+ require("fs-extra");
10
+ const ctl_1 = require("./ctl");
11
+ const resolve_1 = require("./resolve");
12
+ const child_process_1 = require("child_process");
13
+ const chalk_1 = __importDefault(require("chalk"));
14
+ const program = new commander_1.Command();
15
+ const package_json_1 = __importDefault(require("../package.json"));
16
+ program
17
+ .name('agentctl')
18
+ .description('Agent Controller CLI - Unified control plane for humans and agents')
19
+ .version(package_json_1.default.version)
20
+ .allowUnknownOption()
21
+ .helpOption(false) // Disable default help to allow pass-through
22
+ .argument('[command...]', 'Command to run')
23
+ .action(async (args, _options, _command) => {
24
+ // If no args, check for help flag or just show help
25
+ if (!args || args.length === 0) {
26
+ // If they passed --help or -h, show help. If no args at all, show help.
27
+ // Since we ate options, we check raw args or just treat empty args as help.
28
+ // command.opts() won't have help if we disabled it?
29
+ // Actually, if we disable helpOption, --help becomes an unknown option or arg.
30
+ // Let's check process.argv for -h or --help if args is empty?
31
+ // "agentctl --help" -> args=[], options might contain help if we didn't disable it?
32
+ // With helpOption(false), --help is just a flag in argv.
33
+ // If args is empty and we see help flag, show help.
34
+ if (process.argv.includes('--help') || process.argv.includes('-h') || process.argv.length <= 2) {
35
+ program.help();
36
+ return;
37
+ }
38
+ }
39
+ // If args are present, we try to resolve.
40
+ // BUT, "agentctl --help" will result in args being empty if it's parsed as option?
41
+ // Wait, if helpOption(false), then --help is an unknown option.
42
+ // If allowUnknownOption is true, it might not be in 'args' if it looks like a flag.
43
+ // Let's rely on resolveCommand. passed args are variadic.
44
+ // However, "agentctl dev --help" -> args=["dev", "--help"]?
45
+ // My repro says yes: [ 'dev-tools', 'gh', '--help' ].
46
+ // So for "agentctl --help", args might be ["--help"].
47
+ if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
48
+ program.help();
49
+ return;
50
+ }
51
+ // Bypass for ctl subcommand if it slipped through (shouldn't if registered)
52
+ if (args[0] === 'ctl')
53
+ return;
54
+ try {
55
+ // resolveCommand needs to handle flags in args if they are part of the path?
56
+ // No, flags usually come after. resolveCommand stops at first non-matching path part?
57
+ // resolveCommand logic: iterates args.
58
+ // "dev-tools gh --help" -> path "dev-tools gh", remaining "--help"
59
+ const result = await (0, resolve_1.resolveCommand)(args);
60
+ if (!result) {
61
+ // If not found, and they asked for help, show root help?
62
+ // Or if they just typed a wrong command.
63
+ if (args.includes('--help') || args.includes('-h')) {
64
+ // Try to show help for the partial command?
65
+ // For now, just show root list/help or error.
66
+ // If it's "agentctl dev --help" and "dev" is a group, resolveCommand SHOULD return the group.
67
+ }
68
+ console.error(chalk_1.default.red(`Command '${args.join(' ')}' not found.`));
69
+ console.log(`Run ${chalk_1.default.cyan('agentctl list')} to see available commands.`);
70
+ process.exit(1);
71
+ }
72
+ const { manifest, args: remainingArgs, scope } = result;
73
+ if (manifest.run) {
74
+ // ... run logic ...
75
+ // remainingArgs should contain --help if it was passed.
76
+ const cmdDir = path_1.default.dirname(result.manifestPath);
77
+ let runCmd = manifest.run;
78
+ // Resolve relative path
79
+ if (runCmd.startsWith('./') || runCmd.startsWith('.\\')) {
80
+ runCmd = path_1.default.resolve(cmdDir, runCmd);
81
+ }
82
+ // Interpolate {{DIR}}
83
+ runCmd = runCmd.replace(/{{DIR}}/g, cmdDir);
84
+ const fullCommand = `${runCmd} ${remainingArgs.join(' ')}`;
85
+ console.log(chalk_1.default.dim(`[${scope}] Running: ${fullCommand}`));
86
+ const child = (0, child_process_1.spawn)(fullCommand, {
87
+ cwd: process.cwd(), // Execute in CWD as discussed
88
+ shell: true,
89
+ stdio: 'inherit',
90
+ env: { ...process.env, AGENTCTL_SCOPE: scope }
91
+ });
92
+ child.on('exit', (code) => {
93
+ process.exit(code || 0);
94
+ });
95
+ }
96
+ else {
97
+ // Group
98
+ console.log(chalk_1.default.blue(chalk_1.default.bold(`${manifest.name}`)));
99
+ console.log(manifest.description || 'No description');
100
+ console.log('\nSubcommands:');
101
+ const all = await (0, ctl_1.list)();
102
+ const prefix = result.cmdPath + ' ';
103
+ // Filter logic roughly for direct children
104
+ const depth = result.cmdPath.split(' ').length;
105
+ const children = all.filter(c => c.path.startsWith(prefix) && c.path !== result.cmdPath);
106
+ const direct = children.filter(c => c.path.split(' ').length === depth + 1);
107
+ if (direct.length === 0 && children.length === 0) {
108
+ console.log(chalk_1.default.dim(' (No subcommands found)'));
109
+ }
110
+ for (const child of direct) {
111
+ console.log(` ${child.path.split(' ').pop()}\t${chalk_1.default.dim(child.description)}`);
112
+ }
113
+ }
114
+ }
115
+ catch (e) {
116
+ if (e instanceof Error) {
117
+ console.error(chalk_1.default.red(e.message));
118
+ }
119
+ else {
120
+ console.error(chalk_1.default.red('An unknown error occurred'));
121
+ }
122
+ process.exit(1);
123
+ }
124
+ });
125
+ const ctl = program.command('ctl')
126
+ .description('Agent Controller Management - Create, organizing, and managing commands');
127
+ // --- Lifecycle Commands ---
128
+ // We'll stick to flat list but with good descriptions.
129
+ // Helper for consistent error handling
130
+ // Helper for consistent error handling
131
+ const withErrorHandling = (fn) => {
132
+ return async (...args) => {
133
+ try {
134
+ await fn(...args);
135
+ }
136
+ catch (e) {
137
+ if (e instanceof Error) {
138
+ console.error(chalk_1.default.red(e.message));
139
+ }
140
+ else {
141
+ console.error(chalk_1.default.red(String(e)));
142
+ }
143
+ process.exit(1);
144
+ }
145
+ };
146
+ };
147
+ ctl.command('scaffold')
148
+ .description('Create a new capped command with a script file')
149
+ .argument('<path...>', 'Command path segments (e.g., "dev start")')
150
+ .addHelpText('after', `
151
+ Examples:
152
+ $ agentctl ctl scaffold dev start
153
+ $ agentctl ctl scaffold sys backup
154
+ `)
155
+ .action(withErrorHandling(async (pathParts) => {
156
+ await (0, ctl_1.scaffold)(pathParts);
157
+ }));
158
+ ctl.command('alias')
159
+ .description('Create a new capped command that runs an inline shell command')
160
+ .argument('<args...>', 'Name parts followed by target (e.g., "tools" "gh" "gh")')
161
+ .action(withErrorHandling(async (args) => {
162
+ if (args.length < 2) {
163
+ console.error('Usage: ctl alias <name...> <target>');
164
+ process.exit(1);
165
+ }
166
+ const target = args.pop();
167
+ const name = args;
168
+ await (0, ctl_1.alias)(name, target);
169
+ }))
170
+ .addHelpText('after', `
171
+ Examples:
172
+ $ agentctl ctl alias tools gh "gh"
173
+ $ agentctl ctl alias dev build "npm run build"
174
+ `);
175
+ ctl.command('group')
176
+ .description('Create a new command group (namespace)')
177
+ .argument('<path...>', 'Group path (e.g., "dev")')
178
+ .addHelpText('after', `
179
+ Examples:
180
+ $ agentctl ctl group dev
181
+ $ agentctl ctl group tools
182
+ `)
183
+ .action(withErrorHandling(async (parts) => {
184
+ await (0, ctl_1.group)(parts);
185
+ }));
186
+ ctl.command('rm')
187
+ .description('Remove a command or group permanently')
188
+ .argument('<path...>', 'Command path to remove')
189
+ .option('--global', 'Remove from global scope')
190
+ .addHelpText('after', `
191
+ Examples:
192
+ $ agentctl ctl rm dev start
193
+ $ agentctl ctl rm tools --global
194
+ `)
195
+ .action(withErrorHandling(async (parts, opts) => {
196
+ await (0, ctl_1.rm)(parts, { global: opts.global });
197
+ }));
198
+ ctl.command('mv')
199
+ .description('Move a command or group to a new path')
200
+ .argument('<src>', 'Source path (quoted string or single token)')
201
+ .argument('<dest>', 'Destination path')
202
+ .option('--global', 'Move command in global scope')
203
+ .addHelpText('after', `
204
+ Examples:
205
+ $ agentctl ctl mv "dev start" "dev boot"
206
+ $ agentctl ctl mv tools/gh tools/github --global
207
+ `)
208
+ .action(withErrorHandling(async (src, dest, opts) => {
209
+ await (0, ctl_1.mv)(src.split(' '), dest.split(' '), { global: opts.global });
210
+ }));
211
+ // --- Introspection ---
212
+ ctl.command('list')
213
+ .description('List all available commands across local and global scopes')
214
+ .action(withErrorHandling(async () => {
215
+ const items = await (0, ctl_1.list)();
216
+ console.log('TYPE SCOPE COMMAND DESCRIPTION');
217
+ for (const item of items) {
218
+ console.log(`${item.type.padEnd(9)} ${item.scope.padEnd(9)} ${item.path.padEnd(19)} ${item.description}`);
219
+ }
220
+ }));
221
+ ctl.command('inspect')
222
+ .description('Inspect the internal manifest and details of a command')
223
+ .argument('<path...>', 'Command path to inspect')
224
+ .action(withErrorHandling(async (parts) => {
225
+ const info = await (0, ctl_1.inspect)(parts);
226
+ if (info) {
227
+ console.log(JSON.stringify(info, null, 2));
228
+ }
229
+ else {
230
+ console.error('Command not found');
231
+ process.exit(1);
232
+ }
233
+ }));
234
+ // --- Scoping ---
235
+ ctl.command('global')
236
+ .description('Push a local command to the global scope')
237
+ .argument('<path...>', 'Local command path')
238
+ .option('--move', 'Move instead of copy')
239
+ .option('--copy', 'Copy (default)')
240
+ .addHelpText('after', `
241
+ Examples:
242
+ $ agentctl ctl global sys --move
243
+ $ agentctl ctl global tools --copy
244
+ `)
245
+ .action(withErrorHandling(async (parts, opts) => {
246
+ await (0, ctl_1.pushGlobal)(parts, { move: opts.move, copy: opts.copy || !opts.move });
247
+ }));
248
+ ctl.command('local')
249
+ .description('Pull a global command to the local scope')
250
+ .argument('<path...>', 'Global command path')
251
+ .option('--move', 'Move instead of copy')
252
+ .option('--copy', 'Copy (default)')
253
+ .addHelpText('after', `
254
+ Examples:
255
+ $ agentctl ctl local tools --copy
256
+ `)
257
+ .action(withErrorHandling(async (parts, opts) => {
258
+ await (0, ctl_1.pullLocal)(parts, { move: opts.move, copy: opts.copy || !opts.move });
259
+ }));
260
+ // --- Agent Integration ---
261
+ // We attach this to the root `ctl` as options or a sub-command?
262
+ // Original code had it as options on `ctl`. We can make it a command for better help.
263
+ // But sticking to options maintains compatibility. We'll improve the option help.
264
+ ctl.option('--install-skill <agent>', 'Install skill for agent (cursor, antigravity, agentsmd, gemini)')
265
+ .option('--global', 'Install skill globally (for supported agents)')
266
+ .addHelpText('after', `
267
+ Examples:
268
+ $ agentctl ctl --install-skill cursor
269
+ $ agentctl ctl --install-skill antigravity --global
270
+ $ agentctl ctl --install-skill gemini
271
+ `)
272
+ .action(withErrorHandling(async (op, command) => {
273
+ const opts = ctl.opts();
274
+ if (opts.installSkill) {
275
+ await (0, ctl_1.installSkill)(opts.installSkill, { global: opts.global });
276
+ }
277
+ else {
278
+ // If no subcmd and no option, show help
279
+ if (command.args.length === 0) {
280
+ ctl.help();
281
+ }
282
+ }
283
+ }));
284
+ // Inject dynamic commands into root help
285
+ // We need to do this before parsing
286
+ (async () => {
287
+ try {
288
+ const allCommands = await (0, ctl_1.list)();
289
+ const topLevel = allCommands.filter(c => !c.path.includes(' ')); // Only top level
290
+ if (topLevel.length > 0) {
291
+ const lines = [''];
292
+ lines.push('User Commands:');
293
+ for (const cmd of topLevel) {
294
+ // simple padding
295
+ lines.push(` ${cmd.path.padEnd(27)}${cmd.description}`);
296
+ }
297
+ lines.push('');
298
+ program.addHelpText('after', lines.join('\n'));
299
+ }
300
+ }
301
+ catch {
302
+ // Ignore errors during help generation (e.g. if not initialized)
303
+ }
304
+ program.parse(process.argv);
305
+ })();
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readManifest = readManifest;
7
+ exports.isCappedManifest = isCappedManifest;
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ async function readManifest(p) {
10
+ try {
11
+ return await fs_extra_1.default.readJson(p);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ function isCappedManifest(m) {
18
+ return !!m.run || m.type === 'scaffold' || m.type === 'alias';
19
+ }
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveCommand = resolveCommand;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const fs_utils_1 = require("./fs-utils");
10
+ const manifest_1 = require("./manifest");
11
+ async function resolveCommand(args, options = {}) {
12
+ const cwd = options.cwd || process.cwd();
13
+ const localRoot = !options.global ? (0, fs_utils_1.findLocalRoot)(cwd) : null;
14
+ const globalRoot = options.globalDir || (0, fs_utils_1.getGlobalRoot)();
15
+ const localAgentctl = localRoot ? path_1.default.join(localRoot, '.agentctl') : null;
16
+ let currentMatch = null;
17
+ // Iterate through args to find longest match
18
+ for (let i = 0; i < args.length; i++) {
19
+ // Path corresponding to args[0..i]
20
+ const currentArgs = args.slice(0, i + 1);
21
+ const relPath = currentArgs.join(path_1.default.sep);
22
+ const cmdPath = currentArgs.join(' ');
23
+ const localPath = localAgentctl ? path_1.default.join(localAgentctl, relPath) : null;
24
+ const globalPath = path_1.default.join(globalRoot, relPath);
25
+ let localManifest = null;
26
+ let globalManifest = null;
27
+ // check local
28
+ if (localPath && await fs_extra_1.default.pathExists(localPath)) {
29
+ const mPath = path_1.default.join(localPath, 'manifest.json');
30
+ if (await fs_extra_1.default.pathExists(mPath)) {
31
+ localManifest = await (0, manifest_1.readManifest)(mPath);
32
+ }
33
+ if (!localManifest && (await fs_extra_1.default.stat(localPath)).isDirectory()) {
34
+ // Implicit group
35
+ localManifest = { name: args[i], type: 'group' };
36
+ }
37
+ }
38
+ // check global
39
+ if (await fs_extra_1.default.pathExists(globalPath)) {
40
+ const mPath = path_1.default.join(globalPath, 'manifest.json');
41
+ if (await fs_extra_1.default.pathExists(mPath)) {
42
+ globalManifest = await (0, manifest_1.readManifest)(mPath);
43
+ }
44
+ if (!globalManifest && (await fs_extra_1.default.stat(globalPath)).isDirectory()) {
45
+ globalManifest = { name: args[i], type: 'group' };
46
+ }
47
+ }
48
+ if (!localManifest && !globalManifest) {
49
+ break;
50
+ }
51
+ const remainingArgs = args.slice(i + 1);
52
+ // Priority logic
53
+ // 1. Local Capped -> Return Match immediately.
54
+ if (localManifest && (0, manifest_1.isCappedManifest)(localManifest)) {
55
+ return {
56
+ manifest: localManifest,
57
+ manifestPath: path_1.default.join(localPath, 'manifest.json'),
58
+ args: remainingArgs,
59
+ scope: 'local',
60
+ cmdPath
61
+ };
62
+ }
63
+ // 2. Global Capped
64
+ if (globalManifest && (0, manifest_1.isCappedManifest)(globalManifest)) {
65
+ // Check if shadowed by Local Group
66
+ if (localManifest) {
67
+ // Local exists (must be group since checked capped above).
68
+ // Shadowed. Treat as Local Group.
69
+ currentMatch = {
70
+ manifest: localManifest,
71
+ manifestPath: path_1.default.join(localPath, 'manifest.json'),
72
+ args: remainingArgs,
73
+ scope: 'local',
74
+ cmdPath
75
+ };
76
+ }
77
+ else {
78
+ // Not shadowed. Global Capped wins. Return immediately.
79
+ return {
80
+ manifest: globalManifest,
81
+ manifestPath: path_1.default.join(globalPath, 'manifest.json'),
82
+ args: remainingArgs,
83
+ scope: 'global',
84
+ cmdPath
85
+ };
86
+ }
87
+ }
88
+ else {
89
+ // Neither is capped. Both are groups (or one is).
90
+ // Local wins if exists.
91
+ if (localManifest) {
92
+ currentMatch = {
93
+ manifest: localManifest,
94
+ manifestPath: path_1.default.join(localPath, 'manifest.json'),
95
+ args: remainingArgs,
96
+ scope: 'local',
97
+ cmdPath
98
+ };
99
+ }
100
+ else {
101
+ currentMatch = {
102
+ manifest: globalManifest,
103
+ manifestPath: path_1.default.join(globalPath, 'manifest.json'),
104
+ args: remainingArgs,
105
+ scope: 'global',
106
+ cmdPath
107
+ };
108
+ }
109
+ }
110
+ }
111
+ return currentMatch;
112
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SUPPORTED_AGENTS = void 0;
7
+ exports.copySkill = copySkill;
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ exports.SUPPORTED_AGENTS = ['cursor', 'antigravity', 'agentsmd', 'gemini'];
11
+ async function copySkill(targetDir, agent) {
12
+ // We assume the skill file is located at ../skills/agentctl/SKILL.md relative to specific dist/src/ location
13
+ // Or we find it in the project root if running from source.
14
+ // In production (dist), structure might be:
15
+ // dist/index.js
16
+ // skills/agentctl/SKILL.md (if we copy it to dist)
17
+ // Let's try to locate the source SKILL.md
18
+ // If we are in /src, it is in ../skills/agentctl/SKILL.md
19
+ // If we are in /dist/src (tsc default?), it depends on build.
20
+ // Robust finding:
21
+ let sourcePath = path_1.default.resolve(__dirname, '../../skills/agentctl/SKILL.md');
22
+ if (!fs_extra_1.default.existsSync(sourcePath)) {
23
+ // Try looking in src check (dev mode)
24
+ sourcePath = path_1.default.resolve(__dirname, '../skills/agentctl/SKILL.md');
25
+ }
26
+ if (!fs_extra_1.default.existsSync(sourcePath)) {
27
+ // Fallback for when running from dist/src
28
+ sourcePath = path_1.default.resolve(__dirname, '../../../skills/agentctl/SKILL.md');
29
+ }
30
+ if (!fs_extra_1.default.existsSync(sourcePath)) {
31
+ throw new Error(`Could not locate source SKILL.md. Checked: ${path_1.default.resolve(__dirname, '../../skills/agentctl/SKILL.md')}`);
32
+ }
33
+ await fs_extra_1.default.ensureDir(targetDir);
34
+ // Determine filename
35
+ const filename = agent === 'cursor' ? 'agentctl.md' : 'SKILL.md';
36
+ const targetFile = path_1.default.join(targetDir, filename);
37
+ await fs_extra_1.default.copy(sourcePath, targetFile, { overwrite: true });
38
+ return targetFile;
39
+ }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public"
5
5
  },
6
6
  "description": "Agent Controller - A unified interface for humans and AI agents",
7
- "version": "1.0.2",
7
+ "version": "1.0.3",
8
8
  "main": "dist/index.js",
9
9
  "bin": {
10
10
  "agentctl": "./dist/index.js"