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.
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 -412
  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 -133
  83. package/src/commands/init.ts +0 -514
  84. package/src/commands/list.ts +0 -95
  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 -185
  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
@@ -1,419 +0,0 @@
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
-
8
- import { request as httpRequest, type RequestOptions, type OutgoingHttpHeaders } from 'http';
9
- import { request as httpsRequest } from 'https';
10
- import { getRemoteOrThrow, updateRemoteLastConnected, type RemoteHost } from './remotes.js';
11
- import type { ScopeLevel } from './execute.js';
12
- import type { RoutineRunSummary } from './routine-engine.js';
13
-
14
- // ═══════════════════════════════════════════════════════════════════════════
15
- // TYPES
16
- // ═══════════════════════════════════════════════════════════════════════════
17
-
18
- export interface RemoteRunOptions {
19
- /** Package name to execute */
20
- package: string;
21
- /** Command within the package */
22
- command?: string;
23
- /** Arguments to pass */
24
- args?: string[];
25
- /** Environment variables */
26
- env?: Record<string, string>;
27
- /** Standard input to pass */
28
- stdin?: string;
29
- /** Timeout in milliseconds */
30
- timeout?: number;
31
- /** Scope level for execution */
32
- scope?: ScopeLevel;
33
- }
34
-
35
- export interface RemoteRunResult {
36
- success: boolean;
37
- exitCode: number;
38
- stdout?: string;
39
- stderr?: string;
40
- durationMs: number;
41
- error?: {
42
- code: string;
43
- message: string;
44
- details?: Record<string, unknown>;
45
- };
46
- }
47
-
48
- export interface RemoteHealthResult {
49
- status: 'ok';
50
- hostname: string;
51
- version: string;
52
- uptime: number;
53
- }
54
-
55
- export interface RemotePackageInfo {
56
- name: string;
57
- version: string;
58
- description?: string;
59
- commands?: Record<string, { description: string }>;
60
- }
61
-
62
- export interface RemotePackageList {
63
- packages: Array<{
64
- name: string;
65
- version: string;
66
- path: string;
67
- source: 'local' | 'registry';
68
- }>;
69
- }
70
-
71
- // ═══════════════════════════════════════════════════════════════════════════
72
- // ERRORS
73
- // ═══════════════════════════════════════════════════════════════════════════
74
-
75
- export class RemoteConnectionError extends Error {
76
- constructor(
77
- public remoteName: string,
78
- public url: string,
79
- message: string
80
- ) {
81
- super(`Failed to connect to remote "${remoteName}" at ${url}: ${message}`);
82
- this.name = 'RemoteConnectionError';
83
- }
84
- }
85
-
86
- export class RemoteApiError extends Error {
87
- constructor(
88
- public remoteName: string,
89
- public statusCode: number,
90
- public code: string,
91
- message: string,
92
- public details?: Record<string, unknown>
93
- ) {
94
- super(`Remote "${remoteName}" error [${code}]: ${message}`);
95
- this.name = 'RemoteApiError';
96
- }
97
- }
98
-
99
- // ═══════════════════════════════════════════════════════════════════════════
100
- // HTTP CLIENT
101
- // ═══════════════════════════════════════════════════════════════════════════
102
-
103
- interface HttpResponse {
104
- statusCode: number;
105
- body: string;
106
- }
107
-
108
- function makeRequest(
109
- url: URL,
110
- method: string,
111
- headers: Record<string, string>,
112
- body?: string,
113
- timeoutMs: number = 30000
114
- ): Promise<HttpResponse> {
115
- return new Promise((resolve, reject) => {
116
- const isHttps = url.protocol === 'https:';
117
- const requestFn = isHttps ? httpsRequest : httpRequest;
118
-
119
- const reqHeaders: OutgoingHttpHeaders = {
120
- ...headers,
121
- 'Content-Type': 'application/json'
122
- };
123
-
124
- if (body) {
125
- reqHeaders['Content-Length'] = Buffer.byteLength(body);
126
- }
127
-
128
- const options: RequestOptions = {
129
- method,
130
- hostname: url.hostname,
131
- port: url.port || (isHttps ? 443 : 80),
132
- path: url.pathname + url.search,
133
- headers: reqHeaders,
134
- timeout: timeoutMs
135
- };
136
-
137
- const req = requestFn(options, (res) => {
138
- const chunks: Buffer[] = [];
139
- res.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
140
- res.on('end', () => {
141
- resolve({
142
- statusCode: res.statusCode ?? 500,
143
- body: Buffer.concat(chunks).toString('utf-8')
144
- });
145
- });
146
- });
147
-
148
- req.on('error', (err) => {
149
- reject(err);
150
- });
151
-
152
- req.on('timeout', () => {
153
- req.destroy();
154
- reject(new Error('Request timeout'));
155
- });
156
-
157
- if (body) {
158
- req.write(body);
159
- }
160
-
161
- req.end();
162
- });
163
- }
164
-
165
- function buildHeaders(remote: RemoteHost): Record<string, string> {
166
- const headers: Record<string, string> = {
167
- 'Accept': 'application/json'
168
- };
169
-
170
- if (remote.apiKey) {
171
- headers['X-API-Key'] = remote.apiKey;
172
- }
173
-
174
- return headers;
175
- }
176
-
177
- // ═══════════════════════════════════════════════════════════════════════════
178
- // CLIENT FUNCTIONS
179
- // ═══════════════════════════════════════════════════════════════════════════
180
-
181
- /**
182
- * Check health of a remote service
183
- */
184
- export async function remoteHealth(remoteName: string): Promise<RemoteHealthResult> {
185
- const remote = getRemoteOrThrow(remoteName);
186
- const url = new URL('/health', remote.url);
187
-
188
- try {
189
- const response = await makeRequest(url, 'GET', buildHeaders(remote));
190
-
191
- if (response.statusCode !== 200) {
192
- const error = JSON.parse(response.body)?.error;
193
- throw new RemoteApiError(
194
- remoteName,
195
- response.statusCode,
196
- error?.code ?? 'API_ERROR',
197
- error?.message ?? 'Unknown error',
198
- error?.details
199
- );
200
- }
201
-
202
- const data = JSON.parse(response.body) as RemoteHealthResult;
203
- updateRemoteLastConnected(remoteName);
204
-
205
- return data;
206
- } catch (err) {
207
- if (err instanceof RemoteApiError) throw err;
208
- throw new RemoteConnectionError(
209
- remoteName,
210
- remote.url,
211
- err instanceof Error ? err.message : String(err)
212
- );
213
- }
214
- }
215
-
216
- /**
217
- * List packages on a remote
218
- */
219
- export async function remoteListPackages(remoteName: string): Promise<RemotePackageList> {
220
- const remote = getRemoteOrThrow(remoteName);
221
- const url = new URL('/packages', remote.url);
222
-
223
- try {
224
- const response = await makeRequest(url, 'GET', buildHeaders(remote));
225
-
226
- if (response.statusCode !== 200) {
227
- const error = JSON.parse(response.body)?.error;
228
- throw new RemoteApiError(
229
- remoteName,
230
- response.statusCode,
231
- error?.code ?? 'API_ERROR',
232
- error?.message ?? 'Unknown error',
233
- error?.details
234
- );
235
- }
236
-
237
- updateRemoteLastConnected(remoteName);
238
- return JSON.parse(response.body) as RemotePackageList;
239
- } catch (err) {
240
- if (err instanceof RemoteApiError) throw err;
241
- throw new RemoteConnectionError(
242
- remoteName,
243
- remote.url,
244
- err instanceof Error ? err.message : String(err)
245
- );
246
- }
247
- }
248
-
249
- /**
250
- * Get package info from a remote
251
- */
252
- export async function remotePackageInfo(remoteName: string, packageName: string): Promise<RemotePackageInfo | null> {
253
- const remote = getRemoteOrThrow(remoteName);
254
- const url = new URL(`/packages/${encodeURIComponent(packageName)}`, remote.url);
255
-
256
- try {
257
- const response = await makeRequest(url, 'GET', buildHeaders(remote));
258
-
259
- if (response.statusCode === 404) {
260
- return null;
261
- }
262
-
263
- if (response.statusCode !== 200) {
264
- const error = JSON.parse(response.body)?.error;
265
- throw new RemoteApiError(
266
- remoteName,
267
- response.statusCode,
268
- error?.code ?? 'API_ERROR',
269
- error?.message ?? 'Unknown error',
270
- error?.details
271
- );
272
- }
273
-
274
- updateRemoteLastConnected(remoteName);
275
- return JSON.parse(response.body) as RemotePackageInfo;
276
- } catch (err) {
277
- if (err instanceof RemoteApiError) throw err;
278
- throw new RemoteConnectionError(
279
- remoteName,
280
- remote.url,
281
- err instanceof Error ? err.message : String(err)
282
- );
283
- }
284
- }
285
-
286
- /**
287
- * Run a tool on a remote
288
- */
289
- export async function remoteRunTool(remoteName: string, options: RemoteRunOptions): Promise<RemoteRunResult> {
290
- const remote = getRemoteOrThrow(remoteName);
291
- const url = new URL('/run', remote.url);
292
-
293
- const body = JSON.stringify({
294
- package: options.package,
295
- command: options.command,
296
- args: options.args,
297
- env: options.env,
298
- stdin: options.stdin,
299
- timeout: options.timeout,
300
- scope: options.scope
301
- });
302
-
303
- // Use longer timeout for execution (tool timeout + network overhead)
304
- const requestTimeout = (options.timeout ?? 30000) + 10000;
305
-
306
- try {
307
- const response = await makeRequest(url, 'POST', buildHeaders(remote), body, requestTimeout);
308
-
309
- const data = JSON.parse(response.body);
310
-
311
- // Check for API-level error
312
- if (data.error && response.statusCode >= 400 && response.statusCode !== 500) {
313
- throw new RemoteApiError(
314
- remoteName,
315
- response.statusCode,
316
- data.error.code ?? 'API_ERROR',
317
- data.error.message ?? 'Unknown error',
318
- data.error.details
319
- );
320
- }
321
-
322
- updateRemoteLastConnected(remoteName);
323
-
324
- // Return the run result (may indicate success: false for tool failure)
325
- return data as RemoteRunResult;
326
- } catch (err) {
327
- if (err instanceof RemoteApiError) throw err;
328
- throw new RemoteConnectionError(
329
- remoteName,
330
- remote.url,
331
- err instanceof Error ? err.message : String(err)
332
- );
333
- }
334
- }
335
-
336
- /**
337
- * Run a routine on a remote
338
- */
339
- export async function remoteRunRoutine(
340
- remoteName: string,
341
- routineName: string,
342
- vars?: Record<string, string>
343
- ): Promise<RoutineRunSummary> {
344
- const remote = getRemoteOrThrow(remoteName);
345
- const url = new URL(`/routines/${encodeURIComponent(routineName)}/run`, remote.url);
346
-
347
- const body = vars ? JSON.stringify({ vars }) : '{}';
348
-
349
- try {
350
- const response = await makeRequest(url, 'POST', buildHeaders(remote), body, 300000); // 5 min timeout for routines
351
-
352
- if (response.statusCode === 404) {
353
- throw new RemoteApiError(
354
- remoteName,
355
- 404,
356
- 'NOT_FOUND',
357
- `Routine not found: ${routineName}`
358
- );
359
- }
360
-
361
- const data = JSON.parse(response.body);
362
-
363
- if (data.error && response.statusCode >= 400) {
364
- throw new RemoteApiError(
365
- remoteName,
366
- response.statusCode,
367
- data.error.code ?? 'API_ERROR',
368
- data.error.message ?? 'Unknown error',
369
- data.error.details
370
- );
371
- }
372
-
373
- updateRemoteLastConnected(remoteName);
374
- return data as RoutineRunSummary;
375
- } catch (err) {
376
- if (err instanceof RemoteApiError) throw err;
377
- throw new RemoteConnectionError(
378
- remoteName,
379
- remote.url,
380
- err instanceof Error ? err.message : String(err)
381
- );
382
- }
383
- }
384
-
385
- /**
386
- * Test connection to a remote
387
- */
388
- export async function testRemoteConnection(remoteName: string): Promise<{ success: boolean; message: string; details?: Record<string, unknown> }> {
389
- try {
390
- const health = await remoteHealth(remoteName);
391
- return {
392
- success: true,
393
- message: `Connected to ${health.hostname}`,
394
- details: {
395
- hostname: health.hostname,
396
- version: health.version,
397
- uptime: health.uptime
398
- }
399
- };
400
- } catch (err) {
401
- if (err instanceof RemoteConnectionError) {
402
- return {
403
- success: false,
404
- message: err.message
405
- };
406
- }
407
- if (err instanceof RemoteApiError) {
408
- return {
409
- success: false,
410
- message: `${err.code}: ${err.message}`,
411
- details: err.details
412
- };
413
- }
414
- return {
415
- success: false,
416
- message: err instanceof Error ? err.message : String(err)
417
- };
418
- }
419
- }
@@ -1,268 +0,0 @@
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
-
8
- import { readFileSync, writeFileSync, existsSync } from 'fs';
9
- import { resolve } from 'path';
10
- import { CLI4AI_HOME, ensureCli4aiHome } from './config.js';
11
-
12
- // ═══════════════════════════════════════════════════════════════════════════
13
- // TYPES
14
- // ═══════════════════════════════════════════════════════════════════════════
15
-
16
- export interface RemoteHost {
17
- /** Unique name for this remote (e.g., "chrome-server", "gpu-box") */
18
- name: string;
19
- /** Base URL of the remote cli4ai service (e.g., "http://192.168.1.50:4100") */
20
- url: string;
21
- /** Optional API key for authentication */
22
- apiKey?: string;
23
- /** Optional description */
24
- description?: string;
25
- /** When this remote was added */
26
- addedAt: string;
27
- /** Last successful connection time */
28
- lastConnected?: string;
29
- }
30
-
31
- export interface RemotesConfig {
32
- version: 1;
33
- remotes: RemoteHost[];
34
- }
35
-
36
- // ═══════════════════════════════════════════════════════════════════════════
37
- // PATHS
38
- // ═══════════════════════════════════════════════════════════════════════════
39
-
40
- const REMOTES_FILE = resolve(CLI4AI_HOME, 'remotes.json');
41
-
42
- // ═══════════════════════════════════════════════════════════════════════════
43
- // LOADING & SAVING
44
- // ═══════════════════════════════════════════════════════════════════════════
45
-
46
- const DEFAULT_REMOTES_CONFIG: RemotesConfig = {
47
- version: 1,
48
- remotes: []
49
- };
50
-
51
- /**
52
- * Load remotes configuration
53
- */
54
- export function loadRemotesConfig(): RemotesConfig {
55
- ensureCli4aiHome();
56
-
57
- if (!existsSync(REMOTES_FILE)) {
58
- return { ...DEFAULT_REMOTES_CONFIG };
59
- }
60
-
61
- try {
62
- const content = readFileSync(REMOTES_FILE, 'utf-8');
63
- const data = JSON.parse(content);
64
-
65
- // Validate and migrate if needed
66
- if (!data.version || data.version !== 1) {
67
- return { ...DEFAULT_REMOTES_CONFIG };
68
- }
69
-
70
- return data as RemotesConfig;
71
- } catch {
72
- return { ...DEFAULT_REMOTES_CONFIG };
73
- }
74
- }
75
-
76
- /**
77
- * Save remotes configuration
78
- */
79
- export function saveRemotesConfig(config: RemotesConfig): void {
80
- ensureCli4aiHome();
81
- const content = JSON.stringify(config, null, 2) + '\n';
82
- writeFileSync(REMOTES_FILE, content, { mode: 0o600 });
83
- }
84
-
85
- // ═══════════════════════════════════════════════════════════════════════════
86
- // REMOTE MANAGEMENT
87
- // ═══════════════════════════════════════════════════════════════════════════
88
-
89
- export class RemoteNotFoundError extends Error {
90
- constructor(public remoteName: string) {
91
- super(`Remote not found: ${remoteName}`);
92
- this.name = 'RemoteNotFoundError';
93
- }
94
- }
95
-
96
- export class RemoteAlreadyExistsError extends Error {
97
- constructor(public remoteName: string) {
98
- super(`Remote already exists: ${remoteName}`);
99
- this.name = 'RemoteAlreadyExistsError';
100
- }
101
- }
102
-
103
- export class InvalidRemoteUrlError extends Error {
104
- constructor(public url: string, public reason: string) {
105
- super(`Invalid remote URL "${url}": ${reason}`);
106
- this.name = 'InvalidRemoteUrlError';
107
- }
108
- }
109
-
110
- /**
111
- * Validate and normalize a remote URL
112
- */
113
- export function validateRemoteUrl(url: string): string {
114
- // Basic URL validation
115
- let parsed: URL;
116
- try {
117
- parsed = new URL(url);
118
- } catch {
119
- throw new InvalidRemoteUrlError(url, 'Not a valid URL');
120
- }
121
-
122
- // Only allow http and https
123
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
124
- throw new InvalidRemoteUrlError(url, 'Must use http or https protocol');
125
- }
126
-
127
- // Remove trailing slash for consistency
128
- let normalized = parsed.origin;
129
- if (parsed.pathname && parsed.pathname !== '/') {
130
- normalized += parsed.pathname.replace(/\/$/, '');
131
- }
132
-
133
- return normalized;
134
- }
135
-
136
- /**
137
- * Validate remote name (alphanumeric, dashes, underscores)
138
- */
139
- export function validateRemoteName(name: string): void {
140
- if (!name || name.length === 0) {
141
- throw new Error('Remote name cannot be empty');
142
- }
143
-
144
- if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
145
- throw new Error('Remote name must start with a letter and contain only letters, numbers, dashes, and underscores');
146
- }
147
-
148
- if (name.length > 64) {
149
- throw new Error('Remote name must be 64 characters or less');
150
- }
151
- }
152
-
153
- /**
154
- * Get all configured remotes
155
- */
156
- export function getRemotes(): RemoteHost[] {
157
- const config = loadRemotesConfig();
158
- return config.remotes;
159
- }
160
-
161
- /**
162
- * Get a remote by name
163
- */
164
- export function getRemote(name: string): RemoteHost | null {
165
- const config = loadRemotesConfig();
166
- return config.remotes.find(r => r.name === name) ?? null;
167
- }
168
-
169
- /**
170
- * Get a remote by name, throwing if not found
171
- */
172
- export function getRemoteOrThrow(name: string): RemoteHost {
173
- const remote = getRemote(name);
174
- if (!remote) {
175
- throw new RemoteNotFoundError(name);
176
- }
177
- return remote;
178
- }
179
-
180
- /**
181
- * Add a new remote
182
- */
183
- export function addRemote(
184
- name: string,
185
- url: string,
186
- options?: { apiKey?: string; description?: string }
187
- ): RemoteHost {
188
- validateRemoteName(name);
189
- const normalizedUrl = validateRemoteUrl(url);
190
-
191
- const config = loadRemotesConfig();
192
-
193
- // Check for duplicate name
194
- if (config.remotes.some(r => r.name === name)) {
195
- throw new RemoteAlreadyExistsError(name);
196
- }
197
-
198
- const remote: RemoteHost = {
199
- name,
200
- url: normalizedUrl,
201
- apiKey: options?.apiKey,
202
- description: options?.description,
203
- addedAt: new Date().toISOString()
204
- };
205
-
206
- config.remotes.push(remote);
207
- saveRemotesConfig(config);
208
-
209
- return remote;
210
- }
211
-
212
- /**
213
- * Update an existing remote
214
- */
215
- export function updateRemote(
216
- name: string,
217
- updates: { url?: string; apiKey?: string; description?: string }
218
- ): RemoteHost {
219
- const config = loadRemotesConfig();
220
- const index = config.remotes.findIndex(r => r.name === name);
221
-
222
- if (index === -1) {
223
- throw new RemoteNotFoundError(name);
224
- }
225
-
226
- const remote = config.remotes[index];
227
-
228
- if (updates.url !== undefined) {
229
- remote.url = validateRemoteUrl(updates.url);
230
- }
231
- if (updates.apiKey !== undefined) {
232
- remote.apiKey = updates.apiKey || undefined;
233
- }
234
- if (updates.description !== undefined) {
235
- remote.description = updates.description || undefined;
236
- }
237
-
238
- saveRemotesConfig(config);
239
- return remote;
240
- }
241
-
242
- /**
243
- * Remove a remote
244
- */
245
- export function removeRemote(name: string): void {
246
- const config = loadRemotesConfig();
247
- const index = config.remotes.findIndex(r => r.name === name);
248
-
249
- if (index === -1) {
250
- throw new RemoteNotFoundError(name);
251
- }
252
-
253
- config.remotes.splice(index, 1);
254
- saveRemotesConfig(config);
255
- }
256
-
257
- /**
258
- * Update last connected time for a remote
259
- */
260
- export function updateRemoteLastConnected(name: string): void {
261
- const config = loadRemotesConfig();
262
- const remote = config.remotes.find(r => r.name === name);
263
-
264
- if (remote) {
265
- remote.lastConnected = new Date().toISOString();
266
- saveRemotesConfig(config);
267
- }
268
- }