@orchagent/cli 0.1.0
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/README.md +18 -0
- package/dist/commands/agents.js +26 -0
- package/dist/commands/call.js +264 -0
- package/dist/commands/fork.js +42 -0
- package/dist/commands/index.js +33 -0
- package/dist/commands/info.js +88 -0
- package/dist/commands/init.js +101 -0
- package/dist/commands/keys.js +172 -0
- package/dist/commands/llm-config.js +40 -0
- package/dist/commands/login.js +94 -0
- package/dist/commands/publish.js +192 -0
- package/dist/commands/publish.test.js +475 -0
- package/dist/commands/run.js +421 -0
- package/dist/commands/run.test.js +330 -0
- package/dist/commands/search.js +46 -0
- package/dist/commands/skill.js +141 -0
- package/dist/commands/star.js +41 -0
- package/dist/commands/whoami.js +17 -0
- package/dist/index.js +55 -0
- package/dist/lib/analytics.js +27 -0
- package/dist/lib/api.js +179 -0
- package/dist/lib/api.test.js +230 -0
- package/dist/lib/browser-auth.js +278 -0
- package/dist/lib/bundle.js +213 -0
- package/dist/lib/config.js +54 -0
- package/dist/lib/config.test.js +144 -0
- package/dist/lib/errors.js +75 -0
- package/dist/lib/llm.js +252 -0
- package/dist/lib/output.js +50 -0
- package/dist/types.js +2 -0
- package/package.json +59 -0
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ApiError = void 0;
|
|
37
|
+
exports.request = request;
|
|
38
|
+
exports.publicRequest = publicRequest;
|
|
39
|
+
exports.getOrg = getOrg;
|
|
40
|
+
exports.updateOrg = updateOrg;
|
|
41
|
+
exports.listPublicAgents = listPublicAgents;
|
|
42
|
+
exports.getPublicAgent = getPublicAgent;
|
|
43
|
+
exports.listMyAgents = listMyAgents;
|
|
44
|
+
exports.createAgent = createAgent;
|
|
45
|
+
exports.starAgent = starAgent;
|
|
46
|
+
exports.unstarAgent = unstarAgent;
|
|
47
|
+
exports.forkAgent = forkAgent;
|
|
48
|
+
exports.searchAgents = searchAgents;
|
|
49
|
+
exports.fetchLlmKeys = fetchLlmKeys;
|
|
50
|
+
exports.uploadCodeBundle = uploadCodeBundle;
|
|
51
|
+
class ApiError extends Error {
|
|
52
|
+
status;
|
|
53
|
+
payload;
|
|
54
|
+
constructor(message, status, payload) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.status = status;
|
|
57
|
+
this.payload = payload;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.ApiError = ApiError;
|
|
61
|
+
function buildUrl(apiUrl, path) {
|
|
62
|
+
return `${apiUrl.replace(/\/$/, '')}${path}`;
|
|
63
|
+
}
|
|
64
|
+
async function parseError(response) {
|
|
65
|
+
const text = await response.text();
|
|
66
|
+
let payload;
|
|
67
|
+
try {
|
|
68
|
+
payload = JSON.parse(text);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
payload = text;
|
|
72
|
+
}
|
|
73
|
+
const message = typeof payload === 'object' && payload
|
|
74
|
+
? payload.error
|
|
75
|
+
?.message ||
|
|
76
|
+
payload.message ||
|
|
77
|
+
response.statusText
|
|
78
|
+
: response.statusText;
|
|
79
|
+
return new ApiError(message, response.status, payload);
|
|
80
|
+
}
|
|
81
|
+
async function request(config, method, path, options = {}) {
|
|
82
|
+
if (!config.apiKey) {
|
|
83
|
+
throw new ApiError('Missing API key. Run `orchagent login` first.', 401);
|
|
84
|
+
}
|
|
85
|
+
const response = await fetch(buildUrl(config.apiUrl, path), {
|
|
86
|
+
method,
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
89
|
+
...(options.headers ?? {}),
|
|
90
|
+
},
|
|
91
|
+
body: options.body,
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw await parseError(response);
|
|
95
|
+
}
|
|
96
|
+
return (await response.json());
|
|
97
|
+
}
|
|
98
|
+
async function publicRequest(config, path) {
|
|
99
|
+
const response = await fetch(buildUrl(config.apiUrl, path));
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw await parseError(response);
|
|
102
|
+
}
|
|
103
|
+
return (await response.json());
|
|
104
|
+
}
|
|
105
|
+
async function getOrg(config) {
|
|
106
|
+
return request(config, 'GET', '/org');
|
|
107
|
+
}
|
|
108
|
+
async function updateOrg(config, payload) {
|
|
109
|
+
return request(config, 'PATCH', '/org', {
|
|
110
|
+
body: JSON.stringify(payload),
|
|
111
|
+
headers: { 'Content-Type': 'application/json' },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async function listPublicAgents(config) {
|
|
115
|
+
return publicRequest(config, '/public/agents');
|
|
116
|
+
}
|
|
117
|
+
async function getPublicAgent(config, org, agent, version) {
|
|
118
|
+
return publicRequest(config, `/public/agents/${org}/${agent}/${version}`);
|
|
119
|
+
}
|
|
120
|
+
// GitHub-like features
|
|
121
|
+
async function listMyAgents(config) {
|
|
122
|
+
return request(config, 'GET', '/agents');
|
|
123
|
+
}
|
|
124
|
+
async function createAgent(config, data) {
|
|
125
|
+
return request(config, 'POST', '/agents', {
|
|
126
|
+
body: JSON.stringify(data),
|
|
127
|
+
headers: { 'Content-Type': 'application/json' },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function starAgent(config, agentId) {
|
|
131
|
+
await request(config, 'POST', `/agents/${agentId}/star`);
|
|
132
|
+
}
|
|
133
|
+
async function unstarAgent(config, agentId) {
|
|
134
|
+
await request(config, 'DELETE', `/agents/${agentId}/star`);
|
|
135
|
+
}
|
|
136
|
+
async function forkAgent(config, agentId) {
|
|
137
|
+
return request(config, 'POST', `/agents/${agentId}/fork`);
|
|
138
|
+
}
|
|
139
|
+
async function searchAgents(config, query, options) {
|
|
140
|
+
const params = new URLSearchParams();
|
|
141
|
+
if (query)
|
|
142
|
+
params.append('search', query);
|
|
143
|
+
if (options?.sort)
|
|
144
|
+
params.append('sort', options.sort);
|
|
145
|
+
if (options?.tags?.length)
|
|
146
|
+
params.append('tags', options.tags.join(','));
|
|
147
|
+
const queryStr = params.toString();
|
|
148
|
+
return publicRequest(config, `/public/agents${queryStr ? `?${queryStr}` : ''}`);
|
|
149
|
+
}
|
|
150
|
+
async function fetchLlmKeys(config) {
|
|
151
|
+
const result = await request(config, 'GET', '/llm-keys/export');
|
|
152
|
+
return result.keys;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Upload a code bundle for a hosted code agent.
|
|
156
|
+
*/
|
|
157
|
+
async function uploadCodeBundle(config, agentId, bundlePath) {
|
|
158
|
+
if (!config.apiKey) {
|
|
159
|
+
throw new ApiError('Missing API key. Run `orchagent login` first.', 401);
|
|
160
|
+
}
|
|
161
|
+
// Read the bundle file
|
|
162
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs/promises')));
|
|
163
|
+
const buffer = await fs.readFile(bundlePath);
|
|
164
|
+
const blob = new Blob([buffer], { type: 'application/zip' });
|
|
165
|
+
// Create form data
|
|
166
|
+
const formData = new FormData();
|
|
167
|
+
formData.append('file', blob, 'bundle.zip');
|
|
168
|
+
const response = await fetch(`${config.apiUrl.replace(/\/$/, '')}/agents/${agentId}/upload`, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers: {
|
|
171
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
172
|
+
},
|
|
173
|
+
body: formData,
|
|
174
|
+
});
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
throw await parseError(response);
|
|
177
|
+
}
|
|
178
|
+
return (await response.json());
|
|
179
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for API client functions.
|
|
4
|
+
*
|
|
5
|
+
* These tests cover the core API client that all CLI commands depend on:
|
|
6
|
+
* - Request building and authentication
|
|
7
|
+
* - Error parsing
|
|
8
|
+
* - Public vs authenticated requests
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const vitest_1 = require("vitest");
|
|
12
|
+
const api_1 = require("./api");
|
|
13
|
+
// Mock fetch globally
|
|
14
|
+
const mockFetch = vitest_1.vi.fn();
|
|
15
|
+
global.fetch = mockFetch;
|
|
16
|
+
(0, vitest_1.describe)('ApiError', () => {
|
|
17
|
+
(0, vitest_1.it)('includes status code and message', () => {
|
|
18
|
+
const error = new api_1.ApiError('Not found', 404);
|
|
19
|
+
(0, vitest_1.expect)(error.message).toBe('Not found');
|
|
20
|
+
(0, vitest_1.expect)(error.status).toBe(404);
|
|
21
|
+
});
|
|
22
|
+
(0, vitest_1.it)('includes optional payload', () => {
|
|
23
|
+
const payload = { error: { code: 'NOT_FOUND' } };
|
|
24
|
+
const error = new api_1.ApiError('Not found', 404, payload);
|
|
25
|
+
(0, vitest_1.expect)(error.payload).toEqual(payload);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
(0, vitest_1.describe)('request', () => {
|
|
29
|
+
const config = {
|
|
30
|
+
apiKey: 'sk_test_123',
|
|
31
|
+
apiUrl: 'https://api.test.com',
|
|
32
|
+
};
|
|
33
|
+
(0, vitest_1.beforeEach)(() => {
|
|
34
|
+
mockFetch.mockReset();
|
|
35
|
+
});
|
|
36
|
+
(0, vitest_1.afterEach)(() => {
|
|
37
|
+
vitest_1.vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)('adds Authorization header with Bearer token', async () => {
|
|
40
|
+
mockFetch.mockResolvedValueOnce({
|
|
41
|
+
ok: true,
|
|
42
|
+
json: () => Promise.resolve({ data: 'test' }),
|
|
43
|
+
});
|
|
44
|
+
await (0, api_1.request)(config, 'GET', '/test');
|
|
45
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/test', vitest_1.expect.objectContaining({
|
|
46
|
+
method: 'GET',
|
|
47
|
+
headers: vitest_1.expect.objectContaining({
|
|
48
|
+
Authorization: 'Bearer sk_test_123',
|
|
49
|
+
}),
|
|
50
|
+
}));
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.it)('returns parsed JSON response', async () => {
|
|
53
|
+
const responseData = { org: 'test', slug: 'test-org' };
|
|
54
|
+
mockFetch.mockResolvedValueOnce({
|
|
55
|
+
ok: true,
|
|
56
|
+
json: () => Promise.resolve(responseData),
|
|
57
|
+
});
|
|
58
|
+
const result = await (0, api_1.request)(config, 'GET', '/org');
|
|
59
|
+
(0, vitest_1.expect)(result).toEqual(responseData);
|
|
60
|
+
});
|
|
61
|
+
(0, vitest_1.it)('throws ApiError when response not ok', async () => {
|
|
62
|
+
mockFetch.mockResolvedValueOnce({
|
|
63
|
+
ok: false,
|
|
64
|
+
status: 404,
|
|
65
|
+
statusText: 'Not Found',
|
|
66
|
+
text: () => Promise.resolve(JSON.stringify({
|
|
67
|
+
error: { message: 'Agent not found' }
|
|
68
|
+
})),
|
|
69
|
+
});
|
|
70
|
+
await (0, vitest_1.expect)((0, api_1.request)(config, 'GET', '/agents/missing'))
|
|
71
|
+
.rejects.toThrow(api_1.ApiError);
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)('throws ApiError with 401 when no API key', async () => {
|
|
74
|
+
const noKeyConfig = {
|
|
75
|
+
apiKey: undefined,
|
|
76
|
+
apiUrl: 'https://api.test.com',
|
|
77
|
+
};
|
|
78
|
+
await (0, vitest_1.expect)((0, api_1.request)(noKeyConfig, 'GET', '/test'))
|
|
79
|
+
.rejects.toThrow('Missing API key');
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.it)('parses error message from JSON response', async () => {
|
|
82
|
+
mockFetch.mockResolvedValueOnce({
|
|
83
|
+
ok: false,
|
|
84
|
+
status: 403,
|
|
85
|
+
statusText: 'Forbidden',
|
|
86
|
+
text: () => Promise.resolve(JSON.stringify({
|
|
87
|
+
error: { message: 'Access denied to private agent' }
|
|
88
|
+
})),
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
await (0, api_1.request)(config, 'GET', '/agents/private');
|
|
92
|
+
vitest_1.expect.fail('Should have thrown');
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
(0, vitest_1.expect)(error).toBeInstanceOf(api_1.ApiError);
|
|
96
|
+
(0, vitest_1.expect)(error.message).toBe('Access denied to private agent');
|
|
97
|
+
(0, vitest_1.expect)(error.status).toBe(403);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
(0, vitest_1.it)('uses statusText when no JSON message', async () => {
|
|
101
|
+
mockFetch.mockResolvedValueOnce({
|
|
102
|
+
ok: false,
|
|
103
|
+
status: 500,
|
|
104
|
+
statusText: 'Internal Server Error',
|
|
105
|
+
text: () => Promise.resolve('not json'),
|
|
106
|
+
});
|
|
107
|
+
try {
|
|
108
|
+
await (0, api_1.request)(config, 'GET', '/broken');
|
|
109
|
+
vitest_1.expect.fail('Should have thrown');
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
(0, vitest_1.expect)(error).toBeInstanceOf(api_1.ApiError);
|
|
113
|
+
(0, vitest_1.expect)(error.message).toBe('Internal Server Error');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
(0, vitest_1.it)('includes custom headers', async () => {
|
|
117
|
+
mockFetch.mockResolvedValueOnce({
|
|
118
|
+
ok: true,
|
|
119
|
+
json: () => Promise.resolve({}),
|
|
120
|
+
});
|
|
121
|
+
await (0, api_1.request)(config, 'POST', '/agents', {
|
|
122
|
+
body: JSON.stringify({ name: 'test' }),
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
});
|
|
125
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(vitest_1.expect.any(String), vitest_1.expect.objectContaining({
|
|
126
|
+
headers: vitest_1.expect.objectContaining({
|
|
127
|
+
'Content-Type': 'application/json',
|
|
128
|
+
Authorization: 'Bearer sk_test_123',
|
|
129
|
+
}),
|
|
130
|
+
}));
|
|
131
|
+
});
|
|
132
|
+
(0, vitest_1.it)('strips trailing slash from API URL', async () => {
|
|
133
|
+
const configWithSlash = {
|
|
134
|
+
apiKey: 'sk_test_123',
|
|
135
|
+
apiUrl: 'https://api.test.com/',
|
|
136
|
+
};
|
|
137
|
+
mockFetch.mockResolvedValueOnce({
|
|
138
|
+
ok: true,
|
|
139
|
+
json: () => Promise.resolve({}),
|
|
140
|
+
});
|
|
141
|
+
await (0, api_1.request)(configWithSlash, 'GET', '/test');
|
|
142
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/test', vitest_1.expect.any(Object));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
(0, vitest_1.describe)('publicRequest', () => {
|
|
146
|
+
const config = {
|
|
147
|
+
apiUrl: 'https://api.test.com',
|
|
148
|
+
};
|
|
149
|
+
(0, vitest_1.beforeEach)(() => {
|
|
150
|
+
mockFetch.mockReset();
|
|
151
|
+
});
|
|
152
|
+
(0, vitest_1.it)('makes unauthenticated request', async () => {
|
|
153
|
+
mockFetch.mockResolvedValueOnce({
|
|
154
|
+
ok: true,
|
|
155
|
+
json: () => Promise.resolve([]),
|
|
156
|
+
});
|
|
157
|
+
await (0, api_1.publicRequest)(config, '/public/agents');
|
|
158
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/public/agents');
|
|
159
|
+
});
|
|
160
|
+
(0, vitest_1.it)('returns parsed JSON', async () => {
|
|
161
|
+
const agents = [{ name: 'agent1' }, { name: 'agent2' }];
|
|
162
|
+
mockFetch.mockResolvedValueOnce({
|
|
163
|
+
ok: true,
|
|
164
|
+
json: () => Promise.resolve(agents),
|
|
165
|
+
});
|
|
166
|
+
const result = await (0, api_1.publicRequest)(config, '/public/agents');
|
|
167
|
+
(0, vitest_1.expect)(result).toEqual(agents);
|
|
168
|
+
});
|
|
169
|
+
(0, vitest_1.it)('throws ApiError on failure', async () => {
|
|
170
|
+
mockFetch.mockResolvedValueOnce({
|
|
171
|
+
ok: false,
|
|
172
|
+
status: 404,
|
|
173
|
+
statusText: 'Not Found',
|
|
174
|
+
text: () => Promise.resolve('{}'),
|
|
175
|
+
});
|
|
176
|
+
await (0, vitest_1.expect)((0, api_1.publicRequest)(config, '/public/agents/missing'))
|
|
177
|
+
.rejects.toThrow(api_1.ApiError);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
(0, vitest_1.describe)('getOrg', () => {
|
|
181
|
+
const config = {
|
|
182
|
+
apiKey: 'sk_test_123',
|
|
183
|
+
apiUrl: 'https://api.test.com',
|
|
184
|
+
};
|
|
185
|
+
(0, vitest_1.beforeEach)(() => {
|
|
186
|
+
mockFetch.mockReset();
|
|
187
|
+
});
|
|
188
|
+
(0, vitest_1.it)('calls GET /org endpoint', async () => {
|
|
189
|
+
const orgData = { id: '123', slug: 'test-org', name: 'Test Org' };
|
|
190
|
+
mockFetch.mockResolvedValueOnce({
|
|
191
|
+
ok: true,
|
|
192
|
+
json: () => Promise.resolve(orgData),
|
|
193
|
+
});
|
|
194
|
+
const result = await (0, api_1.getOrg)(config);
|
|
195
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/org', vitest_1.expect.objectContaining({ method: 'GET' }));
|
|
196
|
+
(0, vitest_1.expect)(result).toEqual(orgData);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
(0, vitest_1.describe)('searchAgents', () => {
|
|
200
|
+
const config = {
|
|
201
|
+
apiUrl: 'https://api.test.com',
|
|
202
|
+
};
|
|
203
|
+
(0, vitest_1.beforeEach)(() => {
|
|
204
|
+
mockFetch.mockReset();
|
|
205
|
+
});
|
|
206
|
+
(0, vitest_1.it)('builds search query params', async () => {
|
|
207
|
+
mockFetch.mockResolvedValueOnce({
|
|
208
|
+
ok: true,
|
|
209
|
+
json: () => Promise.resolve([]),
|
|
210
|
+
});
|
|
211
|
+
await (0, api_1.searchAgents)(config, 'pdf analyzer', { sort: 'stars' });
|
|
212
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/public/agents?search=pdf+analyzer&sort=stars');
|
|
213
|
+
});
|
|
214
|
+
(0, vitest_1.it)('handles empty query', async () => {
|
|
215
|
+
mockFetch.mockResolvedValueOnce({
|
|
216
|
+
ok: true,
|
|
217
|
+
json: () => Promise.resolve([]),
|
|
218
|
+
});
|
|
219
|
+
await (0, api_1.searchAgents)(config, '');
|
|
220
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/public/agents');
|
|
221
|
+
});
|
|
222
|
+
(0, vitest_1.it)('includes tags in query', async () => {
|
|
223
|
+
mockFetch.mockResolvedValueOnce({
|
|
224
|
+
ok: true,
|
|
225
|
+
json: () => Promise.resolve([]),
|
|
226
|
+
});
|
|
227
|
+
await (0, api_1.searchAgents)(config, 'test', { tags: ['ai', 'document'] });
|
|
228
|
+
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(vitest_1.expect.stringContaining('tags=ai%2Cdocument'));
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,278 @@
|
|
|
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.browserAuthFlow = browserAuthFlow;
|
|
7
|
+
exports.startBrowserAuth = startBrowserAuth;
|
|
8
|
+
const http_1 = __importDefault(require("http"));
|
|
9
|
+
const open_1 = __importDefault(require("open"));
|
|
10
|
+
const errors_1 = require("./errors");
|
|
11
|
+
const DEFAULT_PORT = 8374;
|
|
12
|
+
const AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
13
|
+
/**
|
|
14
|
+
* Perform browser-based OAuth authentication.
|
|
15
|
+
*
|
|
16
|
+
* 1. Starts a local HTTP server to receive the callback
|
|
17
|
+
* 2. Opens the browser to the auth URL
|
|
18
|
+
* 3. Waits for the callback with the one-time token
|
|
19
|
+
* 4. Exchanges the token for an API key
|
|
20
|
+
*/
|
|
21
|
+
async function browserAuthFlow(apiUrl, port = DEFAULT_PORT) {
|
|
22
|
+
// Step 1: Initialize the auth flow
|
|
23
|
+
const initResponse = await fetch(`${apiUrl}/auth/cli-init`, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
body: JSON.stringify({ redirect_port: port }),
|
|
27
|
+
});
|
|
28
|
+
if (!initResponse.ok) {
|
|
29
|
+
const error = await initResponse.json().catch(() => ({}));
|
|
30
|
+
throw new errors_1.CliError(error?.error?.message || `Failed to initialize auth: ${initResponse.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
const { auth_url } = await initResponse.json();
|
|
33
|
+
// Step 2: Start local server and wait for callback
|
|
34
|
+
const token = await waitForCallback(port, AUTH_TIMEOUT_MS);
|
|
35
|
+
// Step 3: Exchange token for API key
|
|
36
|
+
const exchangeResponse = await fetch(`${apiUrl}/auth/cli-exchange`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ token }),
|
|
40
|
+
});
|
|
41
|
+
if (!exchangeResponse.ok) {
|
|
42
|
+
const error = await exchangeResponse.json().catch(() => ({}));
|
|
43
|
+
throw new errors_1.CliError(error?.error?.message || `Failed to exchange token: ${exchangeResponse.statusText}`);
|
|
44
|
+
}
|
|
45
|
+
const result = await exchangeResponse.json();
|
|
46
|
+
// Step 4: Open browser (do this after server is ready but before waiting)
|
|
47
|
+
// Actually we need to open browser earlier, let's restructure
|
|
48
|
+
return {
|
|
49
|
+
apiKey: result.api_key,
|
|
50
|
+
orgSlug: result.org_slug,
|
|
51
|
+
orgName: result.org_name,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Start a local HTTP server, open the browser, and wait for the callback.
|
|
56
|
+
*/
|
|
57
|
+
async function waitForCallback(port, timeoutMs) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
let resolved = false;
|
|
60
|
+
let server = null;
|
|
61
|
+
const cleanup = () => {
|
|
62
|
+
if (server) {
|
|
63
|
+
server.close();
|
|
64
|
+
server = null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const timeout = setTimeout(() => {
|
|
68
|
+
if (!resolved) {
|
|
69
|
+
resolved = true;
|
|
70
|
+
cleanup();
|
|
71
|
+
reject(new errors_1.CliError('Authentication timed out. Please try again.'));
|
|
72
|
+
}
|
|
73
|
+
}, timeoutMs);
|
|
74
|
+
server = http_1.default.createServer((req, res) => {
|
|
75
|
+
// Only handle GET /callback
|
|
76
|
+
const url = new URL(req.url || '/', `http://127.0.0.1:${port}`);
|
|
77
|
+
if (url.pathname !== '/callback') {
|
|
78
|
+
res.writeHead(404);
|
|
79
|
+
res.end('Not Found');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const token = url.searchParams.get('token');
|
|
83
|
+
if (!token) {
|
|
84
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
85
|
+
res.end(errorHtml('Missing token parameter'));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Success!
|
|
89
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
90
|
+
res.end(successHtml());
|
|
91
|
+
if (!resolved) {
|
|
92
|
+
resolved = true;
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
cleanup();
|
|
95
|
+
resolve(token);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
server.on('error', (err) => {
|
|
99
|
+
if (!resolved) {
|
|
100
|
+
resolved = true;
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
cleanup();
|
|
103
|
+
if (err.code === 'EADDRINUSE') {
|
|
104
|
+
reject(new errors_1.CliError(`Port ${port} is already in use. Try a different port with --port.`));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
reject(new errors_1.CliError(`Failed to start auth server: ${err.message}`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// Bind to localhost only for security
|
|
112
|
+
server.listen(port, '127.0.0.1', () => {
|
|
113
|
+
// Server is ready
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Open browser and wait for callback token.
|
|
119
|
+
*/
|
|
120
|
+
async function startBrowserAuth(apiUrl, port = DEFAULT_PORT) {
|
|
121
|
+
// Step 1: Initialize the auth flow
|
|
122
|
+
const initResponse = await fetch(`${apiUrl}/auth/cli-init`, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
body: JSON.stringify({ redirect_port: port }),
|
|
126
|
+
});
|
|
127
|
+
if (!initResponse.ok) {
|
|
128
|
+
const error = await initResponse.json().catch(() => ({}));
|
|
129
|
+
throw new errors_1.CliError(error?.error?.message || `Failed to initialize auth: ${initResponse.statusText}`);
|
|
130
|
+
}
|
|
131
|
+
const { auth_url } = await initResponse.json();
|
|
132
|
+
// Step 2: Start local server to receive callback
|
|
133
|
+
const tokenPromise = waitForCallback(port, AUTH_TIMEOUT_MS);
|
|
134
|
+
// Step 3: Open browser
|
|
135
|
+
try {
|
|
136
|
+
await (0, open_1.default)(auth_url);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// If browser fails to open, show the URL for manual opening
|
|
140
|
+
process.stdout.write(`\nPlease open this URL in your browser:\n${auth_url}\n\n`);
|
|
141
|
+
}
|
|
142
|
+
// Step 4: Wait for callback
|
|
143
|
+
const token = await tokenPromise;
|
|
144
|
+
// Step 5: Exchange token for API key
|
|
145
|
+
const exchangeResponse = await fetch(`${apiUrl}/auth/cli-exchange`, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: { 'Content-Type': 'application/json' },
|
|
148
|
+
body: JSON.stringify({ token }),
|
|
149
|
+
});
|
|
150
|
+
if (!exchangeResponse.ok) {
|
|
151
|
+
const error = await exchangeResponse.json().catch(() => ({}));
|
|
152
|
+
throw new errors_1.CliError(error?.error?.message || `Failed to complete authentication: ${exchangeResponse.statusText}`);
|
|
153
|
+
}
|
|
154
|
+
const result = await exchangeResponse.json();
|
|
155
|
+
return {
|
|
156
|
+
apiKey: result.api_key,
|
|
157
|
+
orgSlug: result.org_slug,
|
|
158
|
+
orgName: result.org_name,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function successHtml() {
|
|
162
|
+
return `<!DOCTYPE html>
|
|
163
|
+
<html>
|
|
164
|
+
<head>
|
|
165
|
+
<meta charset="utf-8">
|
|
166
|
+
<title>OrchAgent CLI - Authentication Successful</title>
|
|
167
|
+
<style>
|
|
168
|
+
body {
|
|
169
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
justify-content: center;
|
|
173
|
+
min-height: 100vh;
|
|
174
|
+
margin: 0;
|
|
175
|
+
background: #0a0a0a;
|
|
176
|
+
color: #fafafa;
|
|
177
|
+
}
|
|
178
|
+
.container {
|
|
179
|
+
text-align: center;
|
|
180
|
+
padding: 2rem;
|
|
181
|
+
}
|
|
182
|
+
.icon {
|
|
183
|
+
width: 64px;
|
|
184
|
+
height: 64px;
|
|
185
|
+
background: rgba(34, 197, 94, 0.1);
|
|
186
|
+
border-radius: 50%;
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
justify-content: center;
|
|
190
|
+
margin: 0 auto 1.5rem;
|
|
191
|
+
}
|
|
192
|
+
.icon svg {
|
|
193
|
+
width: 32px;
|
|
194
|
+
height: 32px;
|
|
195
|
+
color: #22c55e;
|
|
196
|
+
}
|
|
197
|
+
h1 {
|
|
198
|
+
font-size: 1.5rem;
|
|
199
|
+
margin: 0 0 0.5rem;
|
|
200
|
+
}
|
|
201
|
+
p {
|
|
202
|
+
color: #a1a1aa;
|
|
203
|
+
margin: 0;
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
206
|
+
</head>
|
|
207
|
+
<body>
|
|
208
|
+
<div class="container">
|
|
209
|
+
<div class="icon">
|
|
210
|
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
211
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
|
212
|
+
</svg>
|
|
213
|
+
</div>
|
|
214
|
+
<h1>Authentication Successful</h1>
|
|
215
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
216
|
+
</div>
|
|
217
|
+
</body>
|
|
218
|
+
</html>`;
|
|
219
|
+
}
|
|
220
|
+
function errorHtml(message) {
|
|
221
|
+
return `<!DOCTYPE html>
|
|
222
|
+
<html>
|
|
223
|
+
<head>
|
|
224
|
+
<meta charset="utf-8">
|
|
225
|
+
<title>OrchAgent CLI - Authentication Error</title>
|
|
226
|
+
<style>
|
|
227
|
+
body {
|
|
228
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
justify-content: center;
|
|
232
|
+
min-height: 100vh;
|
|
233
|
+
margin: 0;
|
|
234
|
+
background: #0a0a0a;
|
|
235
|
+
color: #fafafa;
|
|
236
|
+
}
|
|
237
|
+
.container {
|
|
238
|
+
text-align: center;
|
|
239
|
+
padding: 2rem;
|
|
240
|
+
}
|
|
241
|
+
.icon {
|
|
242
|
+
width: 64px;
|
|
243
|
+
height: 64px;
|
|
244
|
+
background: rgba(239, 68, 68, 0.1);
|
|
245
|
+
border-radius: 50%;
|
|
246
|
+
display: flex;
|
|
247
|
+
align-items: center;
|
|
248
|
+
justify-content: center;
|
|
249
|
+
margin: 0 auto 1.5rem;
|
|
250
|
+
}
|
|
251
|
+
.icon svg {
|
|
252
|
+
width: 32px;
|
|
253
|
+
height: 32px;
|
|
254
|
+
color: #ef4444;
|
|
255
|
+
}
|
|
256
|
+
h1 {
|
|
257
|
+
font-size: 1.5rem;
|
|
258
|
+
margin: 0 0 0.5rem;
|
|
259
|
+
}
|
|
260
|
+
p {
|
|
261
|
+
color: #a1a1aa;
|
|
262
|
+
margin: 0;
|
|
263
|
+
}
|
|
264
|
+
</style>
|
|
265
|
+
</head>
|
|
266
|
+
<body>
|
|
267
|
+
<div class="container">
|
|
268
|
+
<div class="icon">
|
|
269
|
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
270
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
271
|
+
</svg>
|
|
272
|
+
</div>
|
|
273
|
+
<h1>Authentication Error</h1>
|
|
274
|
+
<p>${message}</p>
|
|
275
|
+
</div>
|
|
276
|
+
</body>
|
|
277
|
+
</html>`;
|
|
278
|
+
}
|