cli4ai 1.1.5 → 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.
Files changed (113) hide show
  1. package/README.md +39 -0
  2. package/dist/bin.d.ts +6 -0
  3. package/dist/bin.js +105 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +335 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.js +459 -0
  8. package/dist/commands/browse.d.ts +4 -0
  9. package/dist/commands/browse.js +379 -0
  10. package/dist/commands/config.d.ts +10 -0
  11. package/dist/commands/config.js +121 -0
  12. package/dist/commands/info.d.ts +9 -0
  13. package/dist/commands/info.js +122 -0
  14. package/dist/commands/init.d.ts +10 -0
  15. package/dist/commands/init.js +458 -0
  16. package/dist/commands/list.d.ts +10 -0
  17. package/dist/commands/list.js +76 -0
  18. package/dist/commands/mcp-config.d.ts +10 -0
  19. package/dist/commands/mcp-config.js +49 -0
  20. package/dist/commands/remotes.d.ts +22 -0
  21. package/dist/commands/remotes.js +196 -0
  22. package/dist/commands/remove.d.ts +8 -0
  23. package/dist/commands/remove.js +61 -0
  24. package/dist/commands/routines.d.ts +29 -0
  25. package/dist/commands/routines.js +363 -0
  26. package/dist/commands/run.d.ts +12 -0
  27. package/dist/commands/run.js +104 -0
  28. package/dist/commands/scheduler.d.ts +27 -0
  29. package/dist/commands/scheduler.js +350 -0
  30. package/dist/commands/search.d.ts +9 -0
  31. package/dist/commands/search.js +159 -0
  32. package/dist/commands/secrets.d.ts +28 -0
  33. package/dist/commands/secrets.js +236 -0
  34. package/dist/commands/serve.d.ts +13 -0
  35. package/dist/commands/serve.js +49 -0
  36. package/dist/commands/start.d.ts +8 -0
  37. package/dist/commands/start.js +27 -0
  38. package/dist/commands/update.d.ts +17 -0
  39. package/dist/commands/update.js +210 -0
  40. package/dist/core/config.d.ts +91 -0
  41. package/dist/core/config.js +738 -0
  42. package/dist/core/execute.d.ts +51 -0
  43. package/dist/core/execute.js +475 -0
  44. package/dist/core/link.d.ts +39 -0
  45. package/dist/core/link.js +214 -0
  46. package/dist/core/lockfile.d.ts +63 -0
  47. package/dist/core/lockfile.js +140 -0
  48. package/dist/core/manifest.d.ts +96 -0
  49. package/dist/core/manifest.js +224 -0
  50. package/dist/core/registry.d.ts +74 -0
  51. package/dist/core/registry.js +116 -0
  52. package/dist/core/remote-client.d.ts +98 -0
  53. package/dist/core/remote-client.js +252 -0
  54. package/dist/core/remotes.d.ts +88 -0
  55. package/dist/core/remotes.js +206 -0
  56. package/dist/core/routine-engine.d.ts +124 -0
  57. package/dist/core/routine-engine.js +699 -0
  58. package/dist/core/routines.d.ts +36 -0
  59. package/dist/core/routines.js +132 -0
  60. package/dist/core/scheduler-daemon.d.ts +10 -0
  61. package/dist/core/scheduler-daemon.js +77 -0
  62. package/dist/core/scheduler.d.ts +131 -0
  63. package/dist/core/scheduler.js +492 -0
  64. package/dist/core/secrets.d.ts +48 -0
  65. package/dist/core/secrets.js +384 -0
  66. package/dist/lib/cli.d.ts +84 -0
  67. package/dist/lib/cli.js +216 -0
  68. package/dist/mcp/adapter.d.ts +35 -0
  69. package/dist/mcp/adapter.js +94 -0
  70. package/dist/mcp/config-gen.d.ts +31 -0
  71. package/dist/mcp/config-gen.js +75 -0
  72. package/dist/mcp/server.d.ts +41 -0
  73. package/dist/mcp/server.js +296 -0
  74. package/dist/server/service.d.ts +85 -0
  75. package/dist/server/service.js +304 -0
  76. package/package.json +6 -3
  77. package/src/bin.ts +0 -118
  78. package/src/cli.ts +0 -409
  79. package/src/commands/add.ts +0 -562
  80. package/src/commands/browse.ts +0 -449
  81. package/src/commands/config.ts +0 -154
  82. package/src/commands/info.ts +0 -102
  83. package/src/commands/init.ts +0 -514
  84. package/src/commands/list.ts +0 -72
  85. package/src/commands/mcp-config.ts +0 -69
  86. package/src/commands/remotes.ts +0 -253
  87. package/src/commands/remove.ts +0 -78
  88. package/src/commands/routines.ts +0 -427
  89. package/src/commands/run.ts +0 -127
  90. package/src/commands/scheduler.ts +0 -438
  91. package/src/commands/search.ts +0 -148
  92. package/src/commands/secrets.ts +0 -292
  93. package/src/commands/serve.ts +0 -66
  94. package/src/commands/start.ts +0 -40
  95. package/src/commands/update.ts +0 -252
  96. package/src/core/config.ts +0 -845
  97. package/src/core/execute.ts +0 -569
  98. package/src/core/link.ts +0 -246
  99. package/src/core/lockfile.ts +0 -187
  100. package/src/core/manifest.ts +0 -327
  101. package/src/core/registry.ts +0 -165
  102. package/src/core/remote-client.ts +0 -419
  103. package/src/core/remotes.ts +0 -268
  104. package/src/core/routine-engine.ts +0 -895
  105. package/src/core/routines.ts +0 -171
  106. package/src/core/scheduler-daemon.ts +0 -94
  107. package/src/core/scheduler.ts +0 -606
  108. package/src/core/secrets.ts +0 -430
  109. package/src/lib/cli.ts +0 -261
  110. package/src/mcp/adapter.ts +0 -131
  111. package/src/mcp/config-gen.ts +0 -106
  112. package/src/mcp/server.ts +0 -365
  113. 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>;