@orchagent/cli 0.3.31 → 0.3.33
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/adapters/agents-md.js +14 -1
- package/dist/adapters/claude-code.js +11 -0
- package/dist/adapters/cursor.js +13 -1
- package/dist/commands/call.js +78 -2
- package/dist/commands/config.js +25 -1
- package/dist/commands/delete.js +11 -20
- package/dist/commands/init.js +139 -22
- package/dist/commands/install.js +16 -3
- package/dist/commands/llm-config.js +40 -0
- package/dist/commands/publish.test.js +475 -0
- package/dist/commands/run.test.js +330 -0
- package/dist/commands/search.js +47 -22
- package/dist/commands/skill.js +10 -7
- package/dist/commands/update.js +101 -66
- package/dist/index.js +0 -0
- package/dist/lib/api.js +68 -2
- package/dist/lib/api.test.js +230 -0
- package/dist/lib/config.js +11 -0
- package/dist/lib/config.test.js +144 -0
- package/dist/lib/output.js +19 -12
- package/dist/lib/skill-resolve.js +99 -0
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for config loading and saving.
|
|
4
|
+
*
|
|
5
|
+
* These tests cover the security-critical config management:
|
|
6
|
+
* - Loading config from file
|
|
7
|
+
* - Saving config with proper permissions
|
|
8
|
+
* - Config resolution priority (overrides > env > file)
|
|
9
|
+
*/
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
const vitest_1 = require("vitest");
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const os_1 = __importDefault(require("os"));
|
|
17
|
+
// Mock fs/promises
|
|
18
|
+
vitest_1.vi.mock('fs/promises', () => ({
|
|
19
|
+
default: {
|
|
20
|
+
readFile: vitest_1.vi.fn(),
|
|
21
|
+
writeFile: vitest_1.vi.fn(),
|
|
22
|
+
mkdir: vitest_1.vi.fn(),
|
|
23
|
+
chmod: vitest_1.vi.fn(),
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
27
|
+
const config_1 = require("./config");
|
|
28
|
+
(0, vitest_1.describe)('loadConfig', () => {
|
|
29
|
+
(0, vitest_1.beforeEach)(() => {
|
|
30
|
+
vitest_1.vi.resetAllMocks();
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.it)('returns empty object when config file missing', async () => {
|
|
33
|
+
const error = new Error('ENOENT');
|
|
34
|
+
error.code = 'ENOENT';
|
|
35
|
+
vitest_1.vi.mocked(promises_1.default.readFile).mockRejectedValueOnce(error);
|
|
36
|
+
const config = await (0, config_1.loadConfig)();
|
|
37
|
+
(0, vitest_1.expect)(config).toEqual({});
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)('parses JSON config file', async () => {
|
|
40
|
+
const configData = {
|
|
41
|
+
api_key: 'sk_test_123',
|
|
42
|
+
api_url: 'https://custom.api.com',
|
|
43
|
+
default_org: 'my-org',
|
|
44
|
+
};
|
|
45
|
+
vitest_1.vi.mocked(promises_1.default.readFile).mockResolvedValueOnce(JSON.stringify(configData));
|
|
46
|
+
const config = await (0, config_1.loadConfig)();
|
|
47
|
+
(0, vitest_1.expect)(config).toEqual(configData);
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.it)('throws on non-ENOENT errors', async () => {
|
|
50
|
+
const error = new Error('Permission denied');
|
|
51
|
+
error.code = 'EACCES';
|
|
52
|
+
vitest_1.vi.mocked(promises_1.default.readFile).mockRejectedValueOnce(error);
|
|
53
|
+
await (0, vitest_1.expect)((0, config_1.loadConfig)()).rejects.toThrow('Permission denied');
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.it)('reads from ~/.orchagent/config.json', async () => {
|
|
56
|
+
vitest_1.vi.mocked(promises_1.default.readFile).mockResolvedValueOnce('{}');
|
|
57
|
+
await (0, config_1.loadConfig)();
|
|
58
|
+
(0, vitest_1.expect)(promises_1.default.readFile).toHaveBeenCalledWith(path_1.default.join(os_1.default.homedir(), '.orchagent', 'config.json'), 'utf-8');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
(0, vitest_1.describe)('saveConfig', () => {
|
|
62
|
+
(0, vitest_1.beforeEach)(() => {
|
|
63
|
+
vitest_1.vi.resetAllMocks();
|
|
64
|
+
vitest_1.vi.mocked(promises_1.default.mkdir).mockResolvedValue(undefined);
|
|
65
|
+
vitest_1.vi.mocked(promises_1.default.writeFile).mockResolvedValue(undefined);
|
|
66
|
+
vitest_1.vi.mocked(promises_1.default.chmod).mockResolvedValue(undefined);
|
|
67
|
+
});
|
|
68
|
+
(0, vitest_1.it)('creates config directory if missing', async () => {
|
|
69
|
+
await (0, config_1.saveConfig)({ api_key: 'sk_test' });
|
|
70
|
+
(0, vitest_1.expect)(promises_1.default.mkdir).toHaveBeenCalledWith(path_1.default.join(os_1.default.homedir(), '.orchagent'), { recursive: true });
|
|
71
|
+
});
|
|
72
|
+
(0, vitest_1.it)('writes JSON with pretty formatting', async () => {
|
|
73
|
+
const config = { api_key: 'sk_test_123' };
|
|
74
|
+
await (0, config_1.saveConfig)(config);
|
|
75
|
+
(0, vitest_1.expect)(promises_1.default.writeFile).toHaveBeenCalledWith(path_1.default.join(os_1.default.homedir(), '.orchagent', 'config.json'), vitest_1.expect.stringContaining('"api_key": "sk_test_123"'), { mode: 0o600 });
|
|
76
|
+
});
|
|
77
|
+
(0, vitest_1.it)('sets restrictive file permissions (0600)', async () => {
|
|
78
|
+
await (0, config_1.saveConfig)({ api_key: 'sk_test' });
|
|
79
|
+
// First via writeFile options
|
|
80
|
+
(0, vitest_1.expect)(promises_1.default.writeFile).toHaveBeenCalledWith(vitest_1.expect.any(String), vitest_1.expect.any(String), { mode: 0o600 });
|
|
81
|
+
// Then explicitly with chmod
|
|
82
|
+
(0, vitest_1.expect)(promises_1.default.chmod).toHaveBeenCalledWith(path_1.default.join(os_1.default.homedir(), '.orchagent', 'config.json'), 0o600);
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.it)('adds trailing newline to file', async () => {
|
|
85
|
+
await (0, config_1.saveConfig)({ api_key: 'test' });
|
|
86
|
+
const writeCall = vitest_1.vi.mocked(promises_1.default.writeFile).mock.calls[0];
|
|
87
|
+
const content = writeCall[1];
|
|
88
|
+
(0, vitest_1.expect)(content.endsWith('\n')).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
(0, vitest_1.describe)('getResolvedConfig', () => {
|
|
92
|
+
const originalEnv = process.env;
|
|
93
|
+
(0, vitest_1.beforeEach)(() => {
|
|
94
|
+
vitest_1.vi.resetAllMocks();
|
|
95
|
+
process.env = { ...originalEnv };
|
|
96
|
+
// Default: no config file
|
|
97
|
+
const error = new Error('ENOENT');
|
|
98
|
+
error.code = 'ENOENT';
|
|
99
|
+
vitest_1.vi.mocked(promises_1.default.readFile).mockRejectedValue(error);
|
|
100
|
+
});
|
|
101
|
+
(0, vitest_1.afterEach)(() => {
|
|
102
|
+
process.env = originalEnv;
|
|
103
|
+
});
|
|
104
|
+
(0, vitest_1.it)('uses default API URL when not configured', async () => {
|
|
105
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
106
|
+
(0, vitest_1.expect)(config.apiUrl).toBe('https://api.orchagent.com');
|
|
107
|
+
});
|
|
108
|
+
(0, vitest_1.it)('reads API key from file config', async () => {
|
|
109
|
+
vitest_1.vi.mocked(promises_1.default.readFile).mockResolvedValueOnce(JSON.stringify({ api_key: 'sk_from_file' }));
|
|
110
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
111
|
+
(0, vitest_1.expect)(config.apiKey).toBe('sk_from_file');
|
|
112
|
+
});
|
|
113
|
+
(0, vitest_1.it)('prioritizes env vars over file config', async () => {
|
|
114
|
+
process.env.ORCHAGENT_API_KEY = 'sk_from_env';
|
|
115
|
+
vitest_1.vi.mocked(promises_1.default.readFile).mockResolvedValueOnce(JSON.stringify({ api_key: 'sk_from_file' }));
|
|
116
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
117
|
+
(0, vitest_1.expect)(config.apiKey).toBe('sk_from_env');
|
|
118
|
+
});
|
|
119
|
+
(0, vitest_1.it)('prioritizes overrides over env vars', async () => {
|
|
120
|
+
process.env.ORCHAGENT_API_KEY = 'sk_from_env';
|
|
121
|
+
const config = await (0, config_1.getResolvedConfig)({ api_key: 'sk_override' });
|
|
122
|
+
(0, vitest_1.expect)(config.apiKey).toBe('sk_override');
|
|
123
|
+
});
|
|
124
|
+
(0, vitest_1.it)('resolves API URL from env var', async () => {
|
|
125
|
+
process.env.ORCHAGENT_API_URL = 'https://custom.api.com';
|
|
126
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
127
|
+
(0, vitest_1.expect)(config.apiUrl).toBe('https://custom.api.com');
|
|
128
|
+
});
|
|
129
|
+
(0, vitest_1.it)('resolves default org from env var', async () => {
|
|
130
|
+
process.env.ORCHAGENT_DEFAULT_ORG = 'my-org';
|
|
131
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
132
|
+
(0, vitest_1.expect)(config.defaultOrg).toBe('my-org');
|
|
133
|
+
});
|
|
134
|
+
(0, vitest_1.it)('returns undefined apiKey when not set anywhere', async () => {
|
|
135
|
+
const config = await (0, config_1.getResolvedConfig)();
|
|
136
|
+
(0, vitest_1.expect)(config.apiKey).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
(0, vitest_1.describe)('getConfigPath', () => {
|
|
140
|
+
(0, vitest_1.it)('returns path to config file', () => {
|
|
141
|
+
const configPath = (0, config_1.getConfigPath)();
|
|
142
|
+
(0, vitest_1.expect)(configPath).toBe(path_1.default.join(os_1.default.homedir(), '.orchagent', 'config.json'));
|
|
143
|
+
});
|
|
144
|
+
});
|
package/dist/lib/output.js
CHANGED
|
@@ -11,17 +11,17 @@ const pricing_1 = require("./pricing");
|
|
|
11
11
|
function printJson(value) {
|
|
12
12
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
13
13
|
}
|
|
14
|
-
function printAgentsTable(agents) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
});
|
|
14
|
+
function printAgentsTable(agents, options) {
|
|
15
|
+
const head = [
|
|
16
|
+
chalk_1.default.bold('Agent'),
|
|
17
|
+
chalk_1.default.bold('Type'),
|
|
18
|
+
...(options?.showVisibility ? [chalk_1.default.bold('Visibility')] : []),
|
|
19
|
+
chalk_1.default.bold('Providers'),
|
|
20
|
+
chalk_1.default.bold('Stars'),
|
|
21
|
+
chalk_1.default.bold('Price'),
|
|
22
|
+
chalk_1.default.bold('Description'),
|
|
23
|
+
];
|
|
24
|
+
const table = new cli_table3_1.default({ head });
|
|
25
25
|
agents.forEach((agent) => {
|
|
26
26
|
const fullName = `${agent.org_slug}/${agent.name}`;
|
|
27
27
|
const type = agent.type || 'code';
|
|
@@ -34,7 +34,14 @@ function printAgentsTable(agents) {
|
|
|
34
34
|
? agent.description.slice(0, 27) + '...'
|
|
35
35
|
: agent.description
|
|
36
36
|
: '-';
|
|
37
|
-
|
|
37
|
+
const visibility = agent.is_public === false
|
|
38
|
+
? chalk_1.default.yellow('private')
|
|
39
|
+
: chalk_1.default.green('public');
|
|
40
|
+
const row = [fullName, type];
|
|
41
|
+
if (options?.showVisibility)
|
|
42
|
+
row.push(visibility);
|
|
43
|
+
row.push(providers, stars.toString(), priceColored, desc);
|
|
44
|
+
table.push(row);
|
|
38
45
|
});
|
|
39
46
|
process.stdout.write(`${table.toString()}\n`);
|
|
40
47
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveSkills = resolveSkills;
|
|
4
|
+
const api_1 = require("./api");
|
|
5
|
+
function parseSkillRef(ref) {
|
|
6
|
+
const [namePart, versionPart] = ref.split('@');
|
|
7
|
+
const version = versionPart?.trim() || 'latest';
|
|
8
|
+
const segments = namePart.split('/');
|
|
9
|
+
if (segments.length !== 2 || !segments[0] || !segments[1]) {
|
|
10
|
+
throw new Error(`Invalid skill reference: ${ref}. Expected format: org/name[@version]`);
|
|
11
|
+
}
|
|
12
|
+
return { org: segments[0], name: segments[1], version };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Download a single skill's content.
|
|
16
|
+
* Tries public endpoint first, falls back to authenticated for private skills.
|
|
17
|
+
* Returns null if the skill can't be found.
|
|
18
|
+
*/
|
|
19
|
+
async function downloadSkill(config, org, name, version) {
|
|
20
|
+
// Try public download endpoint first
|
|
21
|
+
try {
|
|
22
|
+
return await (0, api_1.publicRequest)(config, `/public/agents/${org}/${name}/${version}/download`);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (!(err instanceof api_1.ApiError) || err.status !== 404) {
|
|
26
|
+
// Non-404 errors (network, 500, etc.) - rethrow
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Public not found - try authenticated endpoint for private skills
|
|
31
|
+
if (!config.apiKey) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const userOrg = await (0, api_1.getOrg)(config);
|
|
36
|
+
if (userOrg.slug !== org) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const agents = await (0, api_1.listMyAgents)(config);
|
|
40
|
+
const matching = agents.filter(a => a.name === name && a.type === 'skill');
|
|
41
|
+
if (matching.length === 0) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
let target;
|
|
45
|
+
if (version === 'latest') {
|
|
46
|
+
target = matching.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0];
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const found = matching.find(a => a.version === version);
|
|
50
|
+
if (!found)
|
|
51
|
+
return null;
|
|
52
|
+
target = found;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
name: target.name,
|
|
56
|
+
version: target.version,
|
|
57
|
+
description: target.description,
|
|
58
|
+
prompt: target.prompt,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve an array of skill references to their full content.
|
|
67
|
+
* Fetches each skill from the API and returns resolved skills with prompts.
|
|
68
|
+
* Skills that can't be fetched are skipped with a warning.
|
|
69
|
+
*/
|
|
70
|
+
async function resolveSkills(config, skillRefs, onWarning) {
|
|
71
|
+
const resolved = [];
|
|
72
|
+
for (const ref of skillRefs) {
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = parseSkillRef(ref);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
onWarning?.(`Skipping invalid skill reference: ${ref}`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const skill = await downloadSkill(config, parsed.org, parsed.name, parsed.version);
|
|
83
|
+
if (!skill || !skill.prompt) {
|
|
84
|
+
onWarning?.(`Could not resolve skill '${ref}' (not found or empty)`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
resolved.push({
|
|
88
|
+
ref,
|
|
89
|
+
name: skill.name,
|
|
90
|
+
description: skill.description,
|
|
91
|
+
prompt: skill.prompt,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
onWarning?.(`Could not fetch skill '${ref}': ${err instanceof Error ? err.message : String(err)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return resolved;
|
|
99
|
+
}
|