cli4ai 1.2.0 → 1.2.1
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 +39 -0
- package/dist/bin.d.ts +6 -0
- package/dist/bin.js +105 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +335 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.js +459 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.js +379 -0
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.js +121 -0
- package/dist/commands/info.d.ts +9 -0
- package/dist/commands/info.js +122 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +458 -0
- package/dist/commands/list.d.ts +10 -0
- package/dist/commands/list.js +76 -0
- package/dist/commands/mcp-config.d.ts +10 -0
- package/dist/commands/mcp-config.js +49 -0
- package/dist/commands/remotes.d.ts +22 -0
- package/dist/commands/remotes.js +196 -0
- package/dist/commands/remove.d.ts +8 -0
- package/dist/commands/remove.js +61 -0
- package/dist/commands/routines.d.ts +29 -0
- package/dist/commands/routines.js +363 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +104 -0
- package/dist/commands/scheduler.d.ts +27 -0
- package/dist/commands/scheduler.js +350 -0
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.js +159 -0
- package/dist/commands/secrets.d.ts +28 -0
- package/dist/commands/secrets.js +236 -0
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.js +49 -0
- package/dist/commands/start.d.ts +8 -0
- package/dist/commands/start.js +27 -0
- package/dist/commands/update.d.ts +17 -0
- package/dist/commands/update.js +210 -0
- package/dist/core/config.d.ts +91 -0
- package/dist/core/config.js +738 -0
- package/dist/core/execute.d.ts +51 -0
- package/dist/core/execute.js +475 -0
- package/dist/core/link.d.ts +39 -0
- package/dist/core/link.js +214 -0
- package/dist/core/lockfile.d.ts +63 -0
- package/dist/core/lockfile.js +140 -0
- package/dist/core/manifest.d.ts +96 -0
- package/dist/core/manifest.js +224 -0
- package/dist/core/registry.d.ts +74 -0
- package/dist/core/registry.js +116 -0
- package/dist/core/remote-client.d.ts +98 -0
- package/dist/core/remote-client.js +252 -0
- package/dist/core/remotes.d.ts +88 -0
- package/dist/core/remotes.js +206 -0
- package/dist/core/routine-engine.d.ts +124 -0
- package/dist/core/routine-engine.js +699 -0
- package/dist/core/routines.d.ts +36 -0
- package/dist/core/routines.js +132 -0
- package/dist/core/scheduler-daemon.d.ts +10 -0
- package/dist/core/scheduler-daemon.js +77 -0
- package/dist/core/scheduler.d.ts +131 -0
- package/dist/core/scheduler.js +492 -0
- package/dist/core/secrets.d.ts +48 -0
- package/dist/core/secrets.js +384 -0
- package/dist/lib/cli.d.ts +84 -0
- package/dist/lib/cli.js +216 -0
- package/dist/mcp/adapter.d.ts +35 -0
- package/dist/mcp/adapter.js +94 -0
- package/dist/mcp/config-gen.d.ts +31 -0
- package/dist/mcp/config-gen.js +75 -0
- package/dist/mcp/server.d.ts +41 -0
- package/dist/mcp/server.js +296 -0
- package/dist/server/service.d.ts +85 -0
- package/dist/server/service.js +304 -0
- package/package.json +6 -3
- package/src/bin.ts +0 -118
- package/src/cli.ts +0 -412
- package/src/commands/add.ts +0 -562
- package/src/commands/browse.ts +0 -449
- package/src/commands/config.ts +0 -154
- package/src/commands/info.ts +0 -133
- package/src/commands/init.ts +0 -514
- package/src/commands/list.ts +0 -95
- package/src/commands/mcp-config.ts +0 -69
- package/src/commands/remotes.ts +0 -253
- package/src/commands/remove.ts +0 -78
- package/src/commands/routines.ts +0 -427
- package/src/commands/run.ts +0 -127
- package/src/commands/scheduler.ts +0 -438
- package/src/commands/search.ts +0 -185
- package/src/commands/secrets.ts +0 -292
- package/src/commands/serve.ts +0 -66
- package/src/commands/start.ts +0 -40
- package/src/commands/update.ts +0 -252
- package/src/core/config.ts +0 -845
- package/src/core/execute.ts +0 -569
- package/src/core/link.ts +0 -246
- package/src/core/lockfile.ts +0 -187
- package/src/core/manifest.ts +0 -327
- package/src/core/registry.ts +0 -165
- package/src/core/remote-client.ts +0 -419
- package/src/core/remotes.ts +0 -268
- package/src/core/routine-engine.ts +0 -895
- package/src/core/routines.ts +0 -171
- package/src/core/scheduler-daemon.ts +0 -94
- package/src/core/scheduler.ts +0 -606
- package/src/core/secrets.ts +0 -430
- package/src/lib/cli.ts +0 -261
- package/src/mcp/adapter.ts +0 -131
- package/src/mcp/config-gen.ts +0 -106
- package/src/mcp/server.ts +0 -365
- package/src/server/service.ts +0 -434
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote client for executing cli4ai commands on remote hosts.
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions to call remote cli4ai services
|
|
5
|
+
* configured via `cli4ai remotes add`.
|
|
6
|
+
*/
|
|
7
|
+
import { request as httpRequest } from 'http';
|
|
8
|
+
import { request as httpsRequest } from 'https';
|
|
9
|
+
import { getRemoteOrThrow, updateRemoteLastConnected } from './remotes.js';
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// ERRORS
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
export class RemoteConnectionError extends Error {
|
|
14
|
+
remoteName;
|
|
15
|
+
url;
|
|
16
|
+
constructor(remoteName, url, message) {
|
|
17
|
+
super(`Failed to connect to remote "${remoteName}" at ${url}: ${message}`);
|
|
18
|
+
this.remoteName = remoteName;
|
|
19
|
+
this.url = url;
|
|
20
|
+
this.name = 'RemoteConnectionError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class RemoteApiError extends Error {
|
|
24
|
+
remoteName;
|
|
25
|
+
statusCode;
|
|
26
|
+
code;
|
|
27
|
+
details;
|
|
28
|
+
constructor(remoteName, statusCode, code, message, details) {
|
|
29
|
+
super(`Remote "${remoteName}" error [${code}]: ${message}`);
|
|
30
|
+
this.remoteName = remoteName;
|
|
31
|
+
this.statusCode = statusCode;
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.details = details;
|
|
34
|
+
this.name = 'RemoteApiError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function makeRequest(url, method, headers, body, timeoutMs = 30000) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const isHttps = url.protocol === 'https:';
|
|
40
|
+
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
41
|
+
const reqHeaders = {
|
|
42
|
+
...headers,
|
|
43
|
+
'Content-Type': 'application/json'
|
|
44
|
+
};
|
|
45
|
+
if (body) {
|
|
46
|
+
reqHeaders['Content-Length'] = Buffer.byteLength(body);
|
|
47
|
+
}
|
|
48
|
+
const options = {
|
|
49
|
+
method,
|
|
50
|
+
hostname: url.hostname,
|
|
51
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
52
|
+
path: url.pathname + url.search,
|
|
53
|
+
headers: reqHeaders,
|
|
54
|
+
timeout: timeoutMs
|
|
55
|
+
};
|
|
56
|
+
const req = requestFn(options, (res) => {
|
|
57
|
+
const chunks = [];
|
|
58
|
+
res.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
59
|
+
res.on('end', () => {
|
|
60
|
+
resolve({
|
|
61
|
+
statusCode: res.statusCode ?? 500,
|
|
62
|
+
body: Buffer.concat(chunks).toString('utf-8')
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
req.on('error', (err) => {
|
|
67
|
+
reject(err);
|
|
68
|
+
});
|
|
69
|
+
req.on('timeout', () => {
|
|
70
|
+
req.destroy();
|
|
71
|
+
reject(new Error('Request timeout'));
|
|
72
|
+
});
|
|
73
|
+
if (body) {
|
|
74
|
+
req.write(body);
|
|
75
|
+
}
|
|
76
|
+
req.end();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function buildHeaders(remote) {
|
|
80
|
+
const headers = {
|
|
81
|
+
'Accept': 'application/json'
|
|
82
|
+
};
|
|
83
|
+
if (remote.apiKey) {
|
|
84
|
+
headers['X-API-Key'] = remote.apiKey;
|
|
85
|
+
}
|
|
86
|
+
return headers;
|
|
87
|
+
}
|
|
88
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
89
|
+
// CLIENT FUNCTIONS
|
|
90
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
91
|
+
/**
|
|
92
|
+
* Check health of a remote service
|
|
93
|
+
*/
|
|
94
|
+
export async function remoteHealth(remoteName) {
|
|
95
|
+
const remote = getRemoteOrThrow(remoteName);
|
|
96
|
+
const url = new URL('/health', remote.url);
|
|
97
|
+
try {
|
|
98
|
+
const response = await makeRequest(url, 'GET', buildHeaders(remote));
|
|
99
|
+
if (response.statusCode !== 200) {
|
|
100
|
+
const error = JSON.parse(response.body)?.error;
|
|
101
|
+
throw new RemoteApiError(remoteName, response.statusCode, error?.code ?? 'API_ERROR', error?.message ?? 'Unknown error', error?.details);
|
|
102
|
+
}
|
|
103
|
+
const data = JSON.parse(response.body);
|
|
104
|
+
updateRemoteLastConnected(remoteName);
|
|
105
|
+
return data;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if (err instanceof RemoteApiError)
|
|
109
|
+
throw err;
|
|
110
|
+
throw new RemoteConnectionError(remoteName, remote.url, err instanceof Error ? err.message : String(err));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* List packages on a remote
|
|
115
|
+
*/
|
|
116
|
+
export async function remoteListPackages(remoteName) {
|
|
117
|
+
const remote = getRemoteOrThrow(remoteName);
|
|
118
|
+
const url = new URL('/packages', remote.url);
|
|
119
|
+
try {
|
|
120
|
+
const response = await makeRequest(url, 'GET', buildHeaders(remote));
|
|
121
|
+
if (response.statusCode !== 200) {
|
|
122
|
+
const error = JSON.parse(response.body)?.error;
|
|
123
|
+
throw new RemoteApiError(remoteName, response.statusCode, error?.code ?? 'API_ERROR', error?.message ?? 'Unknown error', error?.details);
|
|
124
|
+
}
|
|
125
|
+
updateRemoteLastConnected(remoteName);
|
|
126
|
+
return JSON.parse(response.body);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (err instanceof RemoteApiError)
|
|
130
|
+
throw err;
|
|
131
|
+
throw new RemoteConnectionError(remoteName, remote.url, err instanceof Error ? err.message : String(err));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get package info from a remote
|
|
136
|
+
*/
|
|
137
|
+
export async function remotePackageInfo(remoteName, packageName) {
|
|
138
|
+
const remote = getRemoteOrThrow(remoteName);
|
|
139
|
+
const url = new URL(`/packages/${encodeURIComponent(packageName)}`, remote.url);
|
|
140
|
+
try {
|
|
141
|
+
const response = await makeRequest(url, 'GET', buildHeaders(remote));
|
|
142
|
+
if (response.statusCode === 404) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (response.statusCode !== 200) {
|
|
146
|
+
const error = JSON.parse(response.body)?.error;
|
|
147
|
+
throw new RemoteApiError(remoteName, response.statusCode, error?.code ?? 'API_ERROR', error?.message ?? 'Unknown error', error?.details);
|
|
148
|
+
}
|
|
149
|
+
updateRemoteLastConnected(remoteName);
|
|
150
|
+
return JSON.parse(response.body);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (err instanceof RemoteApiError)
|
|
154
|
+
throw err;
|
|
155
|
+
throw new RemoteConnectionError(remoteName, remote.url, err instanceof Error ? err.message : String(err));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Run a tool on a remote
|
|
160
|
+
*/
|
|
161
|
+
export async function remoteRunTool(remoteName, options) {
|
|
162
|
+
const remote = getRemoteOrThrow(remoteName);
|
|
163
|
+
const url = new URL('/run', remote.url);
|
|
164
|
+
const body = JSON.stringify({
|
|
165
|
+
package: options.package,
|
|
166
|
+
command: options.command,
|
|
167
|
+
args: options.args,
|
|
168
|
+
env: options.env,
|
|
169
|
+
stdin: options.stdin,
|
|
170
|
+
timeout: options.timeout,
|
|
171
|
+
scope: options.scope
|
|
172
|
+
});
|
|
173
|
+
// Use longer timeout for execution (tool timeout + network overhead)
|
|
174
|
+
const requestTimeout = (options.timeout ?? 30000) + 10000;
|
|
175
|
+
try {
|
|
176
|
+
const response = await makeRequest(url, 'POST', buildHeaders(remote), body, requestTimeout);
|
|
177
|
+
const data = JSON.parse(response.body);
|
|
178
|
+
// Check for API-level error
|
|
179
|
+
if (data.error && response.statusCode >= 400 && response.statusCode !== 500) {
|
|
180
|
+
throw new RemoteApiError(remoteName, response.statusCode, data.error.code ?? 'API_ERROR', data.error.message ?? 'Unknown error', data.error.details);
|
|
181
|
+
}
|
|
182
|
+
updateRemoteLastConnected(remoteName);
|
|
183
|
+
// Return the run result (may indicate success: false for tool failure)
|
|
184
|
+
return data;
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
if (err instanceof RemoteApiError)
|
|
188
|
+
throw err;
|
|
189
|
+
throw new RemoteConnectionError(remoteName, remote.url, err instanceof Error ? err.message : String(err));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Run a routine on a remote
|
|
194
|
+
*/
|
|
195
|
+
export async function remoteRunRoutine(remoteName, routineName, vars) {
|
|
196
|
+
const remote = getRemoteOrThrow(remoteName);
|
|
197
|
+
const url = new URL(`/routines/${encodeURIComponent(routineName)}/run`, remote.url);
|
|
198
|
+
const body = vars ? JSON.stringify({ vars }) : '{}';
|
|
199
|
+
try {
|
|
200
|
+
const response = await makeRequest(url, 'POST', buildHeaders(remote), body, 300000); // 5 min timeout for routines
|
|
201
|
+
if (response.statusCode === 404) {
|
|
202
|
+
throw new RemoteApiError(remoteName, 404, 'NOT_FOUND', `Routine not found: ${routineName}`);
|
|
203
|
+
}
|
|
204
|
+
const data = JSON.parse(response.body);
|
|
205
|
+
if (data.error && response.statusCode >= 400) {
|
|
206
|
+
throw new RemoteApiError(remoteName, response.statusCode, data.error.code ?? 'API_ERROR', data.error.message ?? 'Unknown error', data.error.details);
|
|
207
|
+
}
|
|
208
|
+
updateRemoteLastConnected(remoteName);
|
|
209
|
+
return data;
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
if (err instanceof RemoteApiError)
|
|
213
|
+
throw err;
|
|
214
|
+
throw new RemoteConnectionError(remoteName, remote.url, err instanceof Error ? err.message : String(err));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Test connection to a remote
|
|
219
|
+
*/
|
|
220
|
+
export async function testRemoteConnection(remoteName) {
|
|
221
|
+
try {
|
|
222
|
+
const health = await remoteHealth(remoteName);
|
|
223
|
+
return {
|
|
224
|
+
success: true,
|
|
225
|
+
message: `Connected to ${health.hostname}`,
|
|
226
|
+
details: {
|
|
227
|
+
hostname: health.hostname,
|
|
228
|
+
version: health.version,
|
|
229
|
+
uptime: health.uptime
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (err instanceof RemoteConnectionError) {
|
|
235
|
+
return {
|
|
236
|
+
success: false,
|
|
237
|
+
message: err.message
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (err instanceof RemoteApiError) {
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
message: `${err.code}: ${err.message}`,
|
|
244
|
+
details: err.details
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
message: err instanceof Error ? err.message : String(err)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote host configuration for distributed cli4ai execution.
|
|
3
|
+
*
|
|
4
|
+
* Manages named remote hosts that can execute cli4ai commands.
|
|
5
|
+
* Each remote is a cli4ai instance running `cli4ai serve`.
|
|
6
|
+
*/
|
|
7
|
+
export interface RemoteHost {
|
|
8
|
+
/** Unique name for this remote (e.g., "chrome-server", "gpu-box") */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Base URL of the remote cli4ai service (e.g., "http://192.168.1.50:4100") */
|
|
11
|
+
url: string;
|
|
12
|
+
/** Optional API key for authentication */
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
/** Optional description */
|
|
15
|
+
description?: string;
|
|
16
|
+
/** When this remote was added */
|
|
17
|
+
addedAt: string;
|
|
18
|
+
/** Last successful connection time */
|
|
19
|
+
lastConnected?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface RemotesConfig {
|
|
22
|
+
version: 1;
|
|
23
|
+
remotes: RemoteHost[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Load remotes configuration
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadRemotesConfig(): RemotesConfig;
|
|
29
|
+
/**
|
|
30
|
+
* Save remotes configuration
|
|
31
|
+
*/
|
|
32
|
+
export declare function saveRemotesConfig(config: RemotesConfig): void;
|
|
33
|
+
export declare class RemoteNotFoundError extends Error {
|
|
34
|
+
remoteName: string;
|
|
35
|
+
constructor(remoteName: string);
|
|
36
|
+
}
|
|
37
|
+
export declare class RemoteAlreadyExistsError extends Error {
|
|
38
|
+
remoteName: string;
|
|
39
|
+
constructor(remoteName: string);
|
|
40
|
+
}
|
|
41
|
+
export declare class InvalidRemoteUrlError extends Error {
|
|
42
|
+
url: string;
|
|
43
|
+
reason: string;
|
|
44
|
+
constructor(url: string, reason: string);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Validate and normalize a remote URL
|
|
48
|
+
*/
|
|
49
|
+
export declare function validateRemoteUrl(url: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Validate remote name (alphanumeric, dashes, underscores)
|
|
52
|
+
*/
|
|
53
|
+
export declare function validateRemoteName(name: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* Get all configured remotes
|
|
56
|
+
*/
|
|
57
|
+
export declare function getRemotes(): RemoteHost[];
|
|
58
|
+
/**
|
|
59
|
+
* Get a remote by name
|
|
60
|
+
*/
|
|
61
|
+
export declare function getRemote(name: string): RemoteHost | null;
|
|
62
|
+
/**
|
|
63
|
+
* Get a remote by name, throwing if not found
|
|
64
|
+
*/
|
|
65
|
+
export declare function getRemoteOrThrow(name: string): RemoteHost;
|
|
66
|
+
/**
|
|
67
|
+
* Add a new remote
|
|
68
|
+
*/
|
|
69
|
+
export declare function addRemote(name: string, url: string, options?: {
|
|
70
|
+
apiKey?: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
}): RemoteHost;
|
|
73
|
+
/**
|
|
74
|
+
* Update an existing remote
|
|
75
|
+
*/
|
|
76
|
+
export declare function updateRemote(name: string, updates: {
|
|
77
|
+
url?: string;
|
|
78
|
+
apiKey?: string;
|
|
79
|
+
description?: string;
|
|
80
|
+
}): RemoteHost;
|
|
81
|
+
/**
|
|
82
|
+
* Remove a remote
|
|
83
|
+
*/
|
|
84
|
+
export declare function removeRemote(name: string): void;
|
|
85
|
+
/**
|
|
86
|
+
* Update last connected time for a remote
|
|
87
|
+
*/
|
|
88
|
+
export declare function updateRemoteLastConnected(name: string): void;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote host configuration for distributed cli4ai execution.
|
|
3
|
+
*
|
|
4
|
+
* Manages named remote hosts that can execute cli4ai commands.
|
|
5
|
+
* Each remote is a cli4ai instance running `cli4ai serve`.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
8
|
+
import { resolve } from 'path';
|
|
9
|
+
import { CLI4AI_HOME, ensureCli4aiHome } from './config.js';
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// PATHS
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
const REMOTES_FILE = resolve(CLI4AI_HOME, 'remotes.json');
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
15
|
+
// LOADING & SAVING
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
+
const DEFAULT_REMOTES_CONFIG = {
|
|
18
|
+
version: 1,
|
|
19
|
+
remotes: []
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Load remotes configuration
|
|
23
|
+
*/
|
|
24
|
+
export function loadRemotesConfig() {
|
|
25
|
+
ensureCli4aiHome();
|
|
26
|
+
if (!existsSync(REMOTES_FILE)) {
|
|
27
|
+
return { ...DEFAULT_REMOTES_CONFIG };
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const content = readFileSync(REMOTES_FILE, 'utf-8');
|
|
31
|
+
const data = JSON.parse(content);
|
|
32
|
+
// Validate and migrate if needed
|
|
33
|
+
if (!data.version || data.version !== 1) {
|
|
34
|
+
return { ...DEFAULT_REMOTES_CONFIG };
|
|
35
|
+
}
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { ...DEFAULT_REMOTES_CONFIG };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Save remotes configuration
|
|
44
|
+
*/
|
|
45
|
+
export function saveRemotesConfig(config) {
|
|
46
|
+
ensureCli4aiHome();
|
|
47
|
+
const content = JSON.stringify(config, null, 2) + '\n';
|
|
48
|
+
writeFileSync(REMOTES_FILE, content, { mode: 0o600 });
|
|
49
|
+
}
|
|
50
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
51
|
+
// REMOTE MANAGEMENT
|
|
52
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
53
|
+
export class RemoteNotFoundError extends Error {
|
|
54
|
+
remoteName;
|
|
55
|
+
constructor(remoteName) {
|
|
56
|
+
super(`Remote not found: ${remoteName}`);
|
|
57
|
+
this.remoteName = remoteName;
|
|
58
|
+
this.name = 'RemoteNotFoundError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export class RemoteAlreadyExistsError extends Error {
|
|
62
|
+
remoteName;
|
|
63
|
+
constructor(remoteName) {
|
|
64
|
+
super(`Remote already exists: ${remoteName}`);
|
|
65
|
+
this.remoteName = remoteName;
|
|
66
|
+
this.name = 'RemoteAlreadyExistsError';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export class InvalidRemoteUrlError extends Error {
|
|
70
|
+
url;
|
|
71
|
+
reason;
|
|
72
|
+
constructor(url, reason) {
|
|
73
|
+
super(`Invalid remote URL "${url}": ${reason}`);
|
|
74
|
+
this.url = url;
|
|
75
|
+
this.reason = reason;
|
|
76
|
+
this.name = 'InvalidRemoteUrlError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Validate and normalize a remote URL
|
|
81
|
+
*/
|
|
82
|
+
export function validateRemoteUrl(url) {
|
|
83
|
+
// Basic URL validation
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = new URL(url);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new InvalidRemoteUrlError(url, 'Not a valid URL');
|
|
90
|
+
}
|
|
91
|
+
// Only allow http and https
|
|
92
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
93
|
+
throw new InvalidRemoteUrlError(url, 'Must use http or https protocol');
|
|
94
|
+
}
|
|
95
|
+
// Remove trailing slash for consistency
|
|
96
|
+
let normalized = parsed.origin;
|
|
97
|
+
if (parsed.pathname && parsed.pathname !== '/') {
|
|
98
|
+
normalized += parsed.pathname.replace(/\/$/, '');
|
|
99
|
+
}
|
|
100
|
+
return normalized;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Validate remote name (alphanumeric, dashes, underscores)
|
|
104
|
+
*/
|
|
105
|
+
export function validateRemoteName(name) {
|
|
106
|
+
if (!name || name.length === 0) {
|
|
107
|
+
throw new Error('Remote name cannot be empty');
|
|
108
|
+
}
|
|
109
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
|
|
110
|
+
throw new Error('Remote name must start with a letter and contain only letters, numbers, dashes, and underscores');
|
|
111
|
+
}
|
|
112
|
+
if (name.length > 64) {
|
|
113
|
+
throw new Error('Remote name must be 64 characters or less');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get all configured remotes
|
|
118
|
+
*/
|
|
119
|
+
export function getRemotes() {
|
|
120
|
+
const config = loadRemotesConfig();
|
|
121
|
+
return config.remotes;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get a remote by name
|
|
125
|
+
*/
|
|
126
|
+
export function getRemote(name) {
|
|
127
|
+
const config = loadRemotesConfig();
|
|
128
|
+
return config.remotes.find(r => r.name === name) ?? null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get a remote by name, throwing if not found
|
|
132
|
+
*/
|
|
133
|
+
export function getRemoteOrThrow(name) {
|
|
134
|
+
const remote = getRemote(name);
|
|
135
|
+
if (!remote) {
|
|
136
|
+
throw new RemoteNotFoundError(name);
|
|
137
|
+
}
|
|
138
|
+
return remote;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Add a new remote
|
|
142
|
+
*/
|
|
143
|
+
export function addRemote(name, url, options) {
|
|
144
|
+
validateRemoteName(name);
|
|
145
|
+
const normalizedUrl = validateRemoteUrl(url);
|
|
146
|
+
const config = loadRemotesConfig();
|
|
147
|
+
// Check for duplicate name
|
|
148
|
+
if (config.remotes.some(r => r.name === name)) {
|
|
149
|
+
throw new RemoteAlreadyExistsError(name);
|
|
150
|
+
}
|
|
151
|
+
const remote = {
|
|
152
|
+
name,
|
|
153
|
+
url: normalizedUrl,
|
|
154
|
+
apiKey: options?.apiKey,
|
|
155
|
+
description: options?.description,
|
|
156
|
+
addedAt: new Date().toISOString()
|
|
157
|
+
};
|
|
158
|
+
config.remotes.push(remote);
|
|
159
|
+
saveRemotesConfig(config);
|
|
160
|
+
return remote;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Update an existing remote
|
|
164
|
+
*/
|
|
165
|
+
export function updateRemote(name, updates) {
|
|
166
|
+
const config = loadRemotesConfig();
|
|
167
|
+
const index = config.remotes.findIndex(r => r.name === name);
|
|
168
|
+
if (index === -1) {
|
|
169
|
+
throw new RemoteNotFoundError(name);
|
|
170
|
+
}
|
|
171
|
+
const remote = config.remotes[index];
|
|
172
|
+
if (updates.url !== undefined) {
|
|
173
|
+
remote.url = validateRemoteUrl(updates.url);
|
|
174
|
+
}
|
|
175
|
+
if (updates.apiKey !== undefined) {
|
|
176
|
+
remote.apiKey = updates.apiKey || undefined;
|
|
177
|
+
}
|
|
178
|
+
if (updates.description !== undefined) {
|
|
179
|
+
remote.description = updates.description || undefined;
|
|
180
|
+
}
|
|
181
|
+
saveRemotesConfig(config);
|
|
182
|
+
return remote;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Remove a remote
|
|
186
|
+
*/
|
|
187
|
+
export function removeRemote(name) {
|
|
188
|
+
const config = loadRemotesConfig();
|
|
189
|
+
const index = config.remotes.findIndex(r => r.name === name);
|
|
190
|
+
if (index === -1) {
|
|
191
|
+
throw new RemoteNotFoundError(name);
|
|
192
|
+
}
|
|
193
|
+
config.remotes.splice(index, 1);
|
|
194
|
+
saveRemotesConfig(config);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Update last connected time for a remote
|
|
198
|
+
*/
|
|
199
|
+
export function updateRemoteLastConnected(name) {
|
|
200
|
+
const config = loadRemotesConfig();
|
|
201
|
+
const remote = config.remotes.find(r => r.name === name);
|
|
202
|
+
if (remote) {
|
|
203
|
+
remote.lastConnected = new Date().toISOString();
|
|
204
|
+
saveRemotesConfig(config);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured routine runner (v1).
|
|
3
|
+
*/
|
|
4
|
+
export declare class RoutineParseError extends Error {
|
|
5
|
+
path: string;
|
|
6
|
+
constructor(path: string, message: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class RoutineValidationError extends Error {
|
|
9
|
+
details?: Record<string, unknown> | undefined;
|
|
10
|
+
constructor(message: string, details?: Record<string, unknown> | undefined);
|
|
11
|
+
}
|
|
12
|
+
export declare class RoutineTemplateError extends Error {
|
|
13
|
+
details?: Record<string, unknown> | undefined;
|
|
14
|
+
constructor(message: string, details?: Record<string, unknown> | undefined);
|
|
15
|
+
}
|
|
16
|
+
export type StepCapture = 'inherit' | 'text' | 'json';
|
|
17
|
+
export interface RoutineSchedule {
|
|
18
|
+
/** Cron expression (e.g., "0 9 * * *" for 9am daily) */
|
|
19
|
+
cron?: string;
|
|
20
|
+
/** Simple interval (e.g., "30s", "5m", "1h", "1d") */
|
|
21
|
+
interval?: string;
|
|
22
|
+
/** IANA timezone (e.g., "Pacific/Auckland"). Defaults to system timezone */
|
|
23
|
+
timezone?: string;
|
|
24
|
+
/** Whether this schedule is active. Defaults to true */
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
/** Number of retry attempts on failure. Defaults to 0 */
|
|
27
|
+
retries?: number;
|
|
28
|
+
/** Delay between retries in milliseconds. Defaults to 60000 */
|
|
29
|
+
retryDelayMs?: number;
|
|
30
|
+
/** What to do if previous run is still executing. Defaults to 'skip' */
|
|
31
|
+
concurrency?: 'skip' | 'queue';
|
|
32
|
+
}
|
|
33
|
+
export interface RoutineVarDef {
|
|
34
|
+
default?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface RoutineBaseStep {
|
|
37
|
+
id: string;
|
|
38
|
+
type: 'cli4ai' | 'set' | 'exec';
|
|
39
|
+
continueOnError?: boolean;
|
|
40
|
+
timeout?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface RoutineC4aiStep extends RoutineBaseStep {
|
|
43
|
+
type: 'cli4ai';
|
|
44
|
+
package: string;
|
|
45
|
+
command?: string;
|
|
46
|
+
args?: string[];
|
|
47
|
+
env?: Record<string, string>;
|
|
48
|
+
stdin?: string;
|
|
49
|
+
capture?: StepCapture;
|
|
50
|
+
/** Name of a configured remote to execute on (optional) */
|
|
51
|
+
remote?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface RoutineSetStep extends RoutineBaseStep {
|
|
54
|
+
type: 'set';
|
|
55
|
+
vars: Record<string, string>;
|
|
56
|
+
}
|
|
57
|
+
export interface RoutineExecStep extends RoutineBaseStep {
|
|
58
|
+
type: 'exec';
|
|
59
|
+
cmd: string;
|
|
60
|
+
args?: string[];
|
|
61
|
+
env?: Record<string, string>;
|
|
62
|
+
stdin?: string;
|
|
63
|
+
capture?: Exclude<StepCapture, 'inherit'>;
|
|
64
|
+
}
|
|
65
|
+
export type RoutineStep = RoutineC4aiStep | RoutineSetStep | RoutineExecStep;
|
|
66
|
+
export interface RoutineDefinition {
|
|
67
|
+
version: 1;
|
|
68
|
+
name: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
mcp?: {
|
|
71
|
+
expose?: boolean;
|
|
72
|
+
description?: string;
|
|
73
|
+
};
|
|
74
|
+
vars?: Record<string, RoutineVarDef>;
|
|
75
|
+
/** Schedule configuration for automatic execution */
|
|
76
|
+
schedule?: RoutineSchedule;
|
|
77
|
+
steps: RoutineStep[];
|
|
78
|
+
result?: unknown;
|
|
79
|
+
}
|
|
80
|
+
export interface StepRunResult {
|
|
81
|
+
id: string;
|
|
82
|
+
type: RoutineStep['type'];
|
|
83
|
+
status: 'success' | 'failed' | 'skipped';
|
|
84
|
+
exitCode?: number;
|
|
85
|
+
durationMs?: number;
|
|
86
|
+
stdout?: string;
|
|
87
|
+
stderr?: string;
|
|
88
|
+
json?: unknown;
|
|
89
|
+
error?: {
|
|
90
|
+
code: string;
|
|
91
|
+
message: string;
|
|
92
|
+
details?: Record<string, unknown>;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export interface RoutineRunSummary {
|
|
96
|
+
routine: string;
|
|
97
|
+
version: number;
|
|
98
|
+
status: 'success' | 'failed';
|
|
99
|
+
exitCode: number;
|
|
100
|
+
durationMs: number;
|
|
101
|
+
vars: Record<string, unknown>;
|
|
102
|
+
steps: StepRunResult[];
|
|
103
|
+
result?: unknown;
|
|
104
|
+
}
|
|
105
|
+
export interface RoutineDryRunStep {
|
|
106
|
+
id: string;
|
|
107
|
+
type: RoutineStep['type'];
|
|
108
|
+
rendered: Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
export interface RoutineDryRunPlan {
|
|
111
|
+
routine: string;
|
|
112
|
+
version: number;
|
|
113
|
+
vars: Record<string, unknown>;
|
|
114
|
+
steps: RoutineDryRunStep[];
|
|
115
|
+
result?: unknown;
|
|
116
|
+
}
|
|
117
|
+
export declare function loadRoutineDefinition(path: string): RoutineDefinition;
|
|
118
|
+
/**
|
|
119
|
+
* Validate a schedule configuration.
|
|
120
|
+
* Exported for use by scheduler and tests.
|
|
121
|
+
*/
|
|
122
|
+
export declare function validateScheduleConfig(schedule: unknown, source?: string): RoutineSchedule;
|
|
123
|
+
export declare function dryRunRoutine(def: RoutineDefinition, vars: Record<string, string>, invocationDir: string): Promise<RoutineDryRunPlan>;
|
|
124
|
+
export declare function runRoutine(def: RoutineDefinition, vars: Record<string, string>, invocationDir: string): Promise<RoutineRunSummary>;
|