teleportation-cli 1.0.0 → 1.0.2
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/.claude/hooks/heartbeat.mjs +67 -2
- package/.claude/hooks/permission_request.mjs +55 -26
- package/.claude/hooks/pre_tool_use.mjs +29 -2
- package/.claude/hooks/session-register.mjs +64 -5
- package/.claude/hooks/stop.mjs +205 -1
- package/.claude/hooks/user_prompt_submit.mjs +111 -0
- package/README.md +36 -12
- package/lib/auth/claude-key-extractor.js +196 -0
- package/lib/auth/credentials.js +7 -2
- package/lib/cli/remote-commands.js +649 -0
- package/lib/daemon/teleportation-daemon.js +131 -41
- package/lib/install/installer.js +22 -7
- package/lib/machine-coders/claude-code-adapter.js +191 -37
- package/lib/remote/code-sync.js +213 -0
- package/lib/remote/init-script-robust.js +187 -0
- package/lib/remote/liveport-client.js +417 -0
- package/lib/remote/orchestrator.js +480 -0
- package/lib/remote/pr-creator.js +382 -0
- package/lib/remote/providers/base-provider.js +407 -0
- package/lib/remote/providers/daytona-provider.js +506 -0
- package/lib/remote/providers/fly-provider.js +611 -0
- package/lib/remote/providers/provider-factory.js +228 -0
- package/lib/remote/results-delivery.js +333 -0
- package/lib/remote/session-manager.js +273 -0
- package/lib/remote/state-capture.js +324 -0
- package/lib/remote/vault-client.js +478 -0
- package/lib/session/metadata.js +80 -49
- package/lib/session/mute-checker.js +2 -1
- package/lib/utils/vault-errors.js +353 -0
- package/package.json +5 -5
- package/teleportation-cli.cjs +417 -7
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mech Vault Client for secure credential management
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Storing/retrieving secrets for remote sessions
|
|
6
|
+
* - SSH key generation for remote machine access
|
|
7
|
+
* - Environment file management
|
|
8
|
+
* - Deployment credential bundling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createVaultErrorFromResponse,
|
|
13
|
+
wrapNetworkError,
|
|
14
|
+
VaultRetryExhaustedError,
|
|
15
|
+
VaultValidationError,
|
|
16
|
+
SecretNotFoundError,
|
|
17
|
+
} from '../utils/vault-errors.js';
|
|
18
|
+
|
|
19
|
+
export class VaultClient {
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
this.apiUrl = config.apiUrl || process.env.MECH_VAULT_URL || 'https://vault.mechdna.net/api';
|
|
22
|
+
this.apiKey = config.apiKey || process.env.MECH_API_KEY;
|
|
23
|
+
this.appId = config.appId || process.env.MECH_APP_ID;
|
|
24
|
+
this.maxRetries = config.maxRetries || 3;
|
|
25
|
+
|
|
26
|
+
// Support environment variable override for retry delays
|
|
27
|
+
const defaultRetries = process.env.VAULT_RETRY_DELAYS
|
|
28
|
+
? process.env.VAULT_RETRY_DELAYS.split(',').map(Number)
|
|
29
|
+
: [1000, 2000, 4000]; // Exponential backoff
|
|
30
|
+
this.retryDelays = config.retryDelays || defaultRetries;
|
|
31
|
+
|
|
32
|
+
this.timeout = config.timeout || 30000; // 30 second default timeout
|
|
33
|
+
this.debug = config.debug ?? (process.env.VAULT_DEBUG === 'true');
|
|
34
|
+
|
|
35
|
+
if (!this.apiKey || !this.appId) {
|
|
36
|
+
throw new Error('MECH_API_KEY and MECH_APP_ID are required');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create common headers for all requests
|
|
42
|
+
*/
|
|
43
|
+
_headers() {
|
|
44
|
+
return {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
'X-API-Key': this.apiKey,
|
|
47
|
+
'X-App-ID': this.appId,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Fetch with timeout support
|
|
53
|
+
* @private
|
|
54
|
+
* @param {string} url - URL to fetch
|
|
55
|
+
* @param {Object} options - Fetch options
|
|
56
|
+
* @returns {Promise<Response>} Fetch response
|
|
57
|
+
* @throws {Error} If request times out or fails
|
|
58
|
+
*/
|
|
59
|
+
async _fetchWithTimeout(url, options = {}) {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const response = await fetch(url, {
|
|
65
|
+
...options,
|
|
66
|
+
signal: controller.signal,
|
|
67
|
+
});
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
return response;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
clearTimeout(timeoutId);
|
|
72
|
+
if (error.name === 'AbortError') {
|
|
73
|
+
throw new Error(`Request timed out after ${this.timeout}ms: ${url}`);
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Log debug messages if debug mode is enabled
|
|
81
|
+
* @private
|
|
82
|
+
* @param {string} message - Message to log
|
|
83
|
+
*/
|
|
84
|
+
_log(message) {
|
|
85
|
+
if (this.debug) {
|
|
86
|
+
console.error(`[VaultClient] ${message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate namespace format (alphanumeric, dots, dashes, underscores)
|
|
92
|
+
*/
|
|
93
|
+
_validateNamespace(namespace) {
|
|
94
|
+
if (!namespace || namespace.trim() === '') {
|
|
95
|
+
throw new VaultValidationError('namespace is required');
|
|
96
|
+
}
|
|
97
|
+
const trimmed = namespace.trim();
|
|
98
|
+
const normalized = trimmed
|
|
99
|
+
.replace(/[^a-zA-Z0-9-]+/g, '-')
|
|
100
|
+
.replace(/-+/g, '-')
|
|
101
|
+
.replace(/^-+/, '')
|
|
102
|
+
.replace(/-+$/, '');
|
|
103
|
+
|
|
104
|
+
if (this.debug && normalized && normalized !== trimmed) {
|
|
105
|
+
this._log(`Normalizing namespace from "${trimmed}" to "${normalized}"`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!normalized) {
|
|
109
|
+
throw new VaultValidationError('namespace is required');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!/^[a-zA-Z0-9-]+$/.test(normalized)) {
|
|
113
|
+
throw new VaultValidationError(
|
|
114
|
+
'Invalid namespace format: must contain only alphanumeric characters and dashes'
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return normalized;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Validate secret name format (alphanumeric, dashes, underscores)
|
|
123
|
+
*/
|
|
124
|
+
_validateName(name) {
|
|
125
|
+
if (!name || name.trim() === '') {
|
|
126
|
+
throw new VaultValidationError('name is required');
|
|
127
|
+
}
|
|
128
|
+
const trimmed = name.trim();
|
|
129
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
|
130
|
+
throw new VaultValidationError(
|
|
131
|
+
'Invalid name format: must contain only alphanumeric characters, dashes, and underscores'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return trimmed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Validate secret value is not empty
|
|
139
|
+
*/
|
|
140
|
+
_validateValue(value) {
|
|
141
|
+
if (!value || value.trim() === '') {
|
|
142
|
+
throw new VaultValidationError('value is required');
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Validate secretId is not empty
|
|
149
|
+
*/
|
|
150
|
+
_validateSecretId(secretId) {
|
|
151
|
+
if (!secretId || secretId.trim() === '') {
|
|
152
|
+
throw new VaultValidationError('secretId is required');
|
|
153
|
+
}
|
|
154
|
+
return secretId;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Validate expiresAt is in the future
|
|
159
|
+
*/
|
|
160
|
+
_validateExpiresAt(expiresAt) {
|
|
161
|
+
if (expiresAt) {
|
|
162
|
+
const expiryDate = new Date(expiresAt);
|
|
163
|
+
if (expiryDate <= new Date()) {
|
|
164
|
+
throw new VaultValidationError('expiresAt must be in the future');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Execute fetch with retry logic and exponential backoff
|
|
171
|
+
*/
|
|
172
|
+
async _fetchWithRetry(url, options, retryCount = 0) {
|
|
173
|
+
try {
|
|
174
|
+
const response = await this._fetchWithTimeout(url, options);
|
|
175
|
+
|
|
176
|
+
// Don't retry on client errors (4xx)
|
|
177
|
+
if (!response.ok && response.status >= 400 && response.status < 500) {
|
|
178
|
+
let responseBody;
|
|
179
|
+
try {
|
|
180
|
+
responseBody = await response.text();
|
|
181
|
+
} catch {
|
|
182
|
+
responseBody = undefined;
|
|
183
|
+
}
|
|
184
|
+
throw createVaultErrorFromResponse(response, { url, responseBody });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Retry on server errors (5xx)
|
|
188
|
+
if (!response.ok && response.status >= 500) {
|
|
189
|
+
let responseBody;
|
|
190
|
+
try {
|
|
191
|
+
responseBody = await response.text();
|
|
192
|
+
} catch {
|
|
193
|
+
responseBody = undefined;
|
|
194
|
+
}
|
|
195
|
+
const serverError = createVaultErrorFromResponse(response, { url, responseBody });
|
|
196
|
+
if (retryCount < this.maxRetries - 1) {
|
|
197
|
+
const delay = this.retryDelays[retryCount] || 4000;
|
|
198
|
+
this._log(`Retry attempt ${retryCount + 1} after ${delay}ms due to ${response.status} ${response.statusText}`);
|
|
199
|
+
await this._delay(delay);
|
|
200
|
+
return this._fetchWithRetry(url, options, retryCount + 1);
|
|
201
|
+
}
|
|
202
|
+
throw serverError;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return response;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
// Check if error is a VaultError by checking if it has our custom properties
|
|
208
|
+
// VaultErrors have a code property (VAULT_*, SECRET_*, etc.)
|
|
209
|
+
const isVaultError = error.code && typeof error.statusCode === 'number';
|
|
210
|
+
|
|
211
|
+
// Don't retry client errors (4xx) - they won't succeed on retry
|
|
212
|
+
if (isVaultError && error.statusCode >= 400 && error.statusCode < 500) {
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Retry server errors (5xx)
|
|
217
|
+
if (isVaultError && error.statusCode >= 500) {
|
|
218
|
+
if (retryCount < this.maxRetries - 1) {
|
|
219
|
+
const delay = this.retryDelays[retryCount] || 4000;
|
|
220
|
+
this._log(`Retry attempt ${retryCount + 1} after ${delay}ms due to server error`);
|
|
221
|
+
await this._delay(delay);
|
|
222
|
+
return this._fetchWithRetry(url, options, retryCount + 1);
|
|
223
|
+
}
|
|
224
|
+
// Max retries reached for server error
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Network errors (not VaultErrors) - retry with exponential backoff
|
|
229
|
+
if (!isVaultError) {
|
|
230
|
+
if (retryCount < this.maxRetries - 1) {
|
|
231
|
+
const delay = this.retryDelays[retryCount] || 4000;
|
|
232
|
+
this._log(`Retry attempt ${retryCount + 1} after ${delay}ms due to: ${error.message}`);
|
|
233
|
+
await this._delay(delay);
|
|
234
|
+
return this._fetchWithRetry(url, options, retryCount + 1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Exhausted retries - wrap network error
|
|
238
|
+
const networkError = wrapNetworkError(error, { url, vaultUrl: this.apiUrl });
|
|
239
|
+
throw new VaultRetryExhaustedError(networkError, this.maxRetries);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Other errors - don't retry
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Delay helper for exponential backoff
|
|
249
|
+
*/
|
|
250
|
+
_delay(ms) {
|
|
251
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Store a secret for a remote session
|
|
256
|
+
* @param {string} namespace - e.g., "teleportation.remote.session-abc123"
|
|
257
|
+
* @param {string} name - Secret name (e.g., "github_token")
|
|
258
|
+
* @param {string} value - Secret value
|
|
259
|
+
* @param {Object} options - { description, tags, expiresAt, metadata }
|
|
260
|
+
*/
|
|
261
|
+
async storeSecret(namespace, name, value, options = {}) {
|
|
262
|
+
// Validate inputs
|
|
263
|
+
const validNamespace = this._validateNamespace(namespace);
|
|
264
|
+
const validName = this._validateName(name);
|
|
265
|
+
const validValue = this._validateValue(value);
|
|
266
|
+
this._validateExpiresAt(options.expiresAt);
|
|
267
|
+
|
|
268
|
+
const response = await this._fetchWithRetry(`${this.apiUrl}/secrets`, {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
headers: this._headers(),
|
|
271
|
+
body: JSON.stringify({
|
|
272
|
+
namespace: validNamespace,
|
|
273
|
+
name: validName,
|
|
274
|
+
value: validValue,
|
|
275
|
+
description: options.description,
|
|
276
|
+
tags: options.tags || [],
|
|
277
|
+
expiresAt: options.expiresAt, // ISO 8601 date
|
|
278
|
+
metadata: options.metadata || {},
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return response.json();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Retrieve a secret value (triggers audit log)
|
|
287
|
+
*/
|
|
288
|
+
async getSecretValue(secretId) {
|
|
289
|
+
const validSecretId = this._validateSecretId(secretId);
|
|
290
|
+
|
|
291
|
+
const response = await this._fetchWithRetry(`${this.apiUrl}/secrets/${validSecretId}/value`, {
|
|
292
|
+
method: 'GET',
|
|
293
|
+
headers: this._headers(),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return response.json();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* List secrets by namespace
|
|
301
|
+
*/
|
|
302
|
+
async listSecrets(namespace, options = {}) {
|
|
303
|
+
const validNamespace = this._validateNamespace(namespace);
|
|
304
|
+
|
|
305
|
+
const params = new URLSearchParams({
|
|
306
|
+
namespace: validNamespace,
|
|
307
|
+
...options, // page, limit, tags
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const response = await this._fetchWithRetry(`${this.apiUrl}/secrets?${params}`, {
|
|
311
|
+
method: 'GET',
|
|
312
|
+
headers: this._headers(),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
return response.json();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Generate SSH key pair for remote machine access
|
|
320
|
+
* @param {string} keyType - "ed25519" or "rsa"
|
|
321
|
+
* @param {Object} metadata - Session info
|
|
322
|
+
*/
|
|
323
|
+
async generateSSHKey(keyType = 'ed25519', metadata = {}) {
|
|
324
|
+
const {
|
|
325
|
+
namespace,
|
|
326
|
+
name,
|
|
327
|
+
metadata: _explicitMetadata,
|
|
328
|
+
..._metadataFields
|
|
329
|
+
} = metadata && typeof metadata === 'object' ? metadata : {};
|
|
330
|
+
|
|
331
|
+
const sanitizeAlphaNumeric = (value, fallback) => {
|
|
332
|
+
const raw = typeof value === 'string' && value.trim() ? value.trim() : fallback;
|
|
333
|
+
const cleaned = String(raw).replace(/[^a-zA-Z0-9]/g, '');
|
|
334
|
+
return cleaned || String(fallback).replace(/[^a-zA-Z0-9]/g, '') || 'teleportation';
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const requestNamespace = sanitizeAlphaNumeric(namespace, 'teleportationremote');
|
|
338
|
+
const requestName = sanitizeAlphaNumeric(name, `session${Date.now()}`);
|
|
339
|
+
|
|
340
|
+
const response = await this._fetchWithRetry(`${this.apiUrl}/ssh-keys/generate`, {
|
|
341
|
+
method: 'POST',
|
|
342
|
+
headers: this._headers(),
|
|
343
|
+
body: JSON.stringify({
|
|
344
|
+
keyType,
|
|
345
|
+
namespace: requestNamespace,
|
|
346
|
+
name: requestName,
|
|
347
|
+
}),
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return response.json();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get public key only (for adding to remote machine)
|
|
355
|
+
*/
|
|
356
|
+
async getPublicKey(keyId) {
|
|
357
|
+
const validKeyId = this._validateSecretId(keyId);
|
|
358
|
+
|
|
359
|
+
const response = await this._fetchWithRetry(`${this.apiUrl}/ssh-keys/${validKeyId}/public-key`, {
|
|
360
|
+
method: 'GET',
|
|
361
|
+
headers: this._headers(),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return response.json();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Bundle all secrets needed for deployment
|
|
369
|
+
* @param {string[]} secretIds - Array of secret IDs
|
|
370
|
+
*/
|
|
371
|
+
async bundleDeploymentSecrets(secretIds) {
|
|
372
|
+
if (!Array.isArray(secretIds)) {
|
|
373
|
+
throw new VaultValidationError('secretIds must be an array');
|
|
374
|
+
}
|
|
375
|
+
if (secretIds.length === 0) {
|
|
376
|
+
throw new VaultValidationError('secretIds cannot be empty');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const response = await this._fetchWithRetry(`${this.apiUrl}/deployment/secrets`, {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
headers: this._headers(),
|
|
382
|
+
body: JSON.stringify({ secretIds }),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return response.json();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Export secrets as environment file for remote machine
|
|
390
|
+
* @param {string} namespace - Namespace to export
|
|
391
|
+
* @param {string} format - "env" or "json"
|
|
392
|
+
*/
|
|
393
|
+
async exportEnvFile(namespace, format = 'env') {
|
|
394
|
+
const validNamespace = this._validateNamespace(namespace);
|
|
395
|
+
|
|
396
|
+
const response = await this._fetchWithRetry(`${this.apiUrl}/env-files/export`, {
|
|
397
|
+
method: 'POST',
|
|
398
|
+
headers: this._headers(),
|
|
399
|
+
body: JSON.stringify({
|
|
400
|
+
namespace: validNamespace,
|
|
401
|
+
format,
|
|
402
|
+
}),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return response.text(); // Returns .env file content
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Import local .env file to vault
|
|
410
|
+
* @param {string} envFileContent - Contents of .env file
|
|
411
|
+
* @param {string} namespace - Target namespace
|
|
412
|
+
*/
|
|
413
|
+
async importEnvFile(envFileContent, namespace) {
|
|
414
|
+
if (!envFileContent || envFileContent.trim() === '') {
|
|
415
|
+
throw new VaultValidationError('envFileContent is required');
|
|
416
|
+
}
|
|
417
|
+
const validNamespace = this._validateNamespace(namespace);
|
|
418
|
+
|
|
419
|
+
const response = await this._fetchWithRetry(`${this.apiUrl}/env-files/import`, {
|
|
420
|
+
method: 'POST',
|
|
421
|
+
headers: this._headers(),
|
|
422
|
+
body: JSON.stringify({
|
|
423
|
+
content: envFileContent,
|
|
424
|
+
namespace: validNamespace,
|
|
425
|
+
}),
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
return response.json();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Delete secret (cleanup after session ends)
|
|
433
|
+
*/
|
|
434
|
+
async deleteSecret(secretId) {
|
|
435
|
+
const validSecretId = this._validateSecretId(secretId);
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
await this._fetchWithRetry(`${this.apiUrl}/secrets/${validSecretId}`, {
|
|
439
|
+
method: 'DELETE',
|
|
440
|
+
headers: this._headers(),
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return true;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
// Handle 404 gracefully - secret already deleted
|
|
446
|
+
if (error instanceof SecretNotFoundError || error.statusCode === 404) {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get access logs for audit
|
|
455
|
+
*/
|
|
456
|
+
async getAccessLogs(secretId) {
|
|
457
|
+
const validSecretId = this._validateSecretId(secretId);
|
|
458
|
+
|
|
459
|
+
const response = await this._fetchWithRetry(`${this.apiUrl}/secrets/${validSecretId}/access-logs`, {
|
|
460
|
+
method: 'GET',
|
|
461
|
+
headers: this._headers(),
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return response.json();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Cleanup expired secrets
|
|
469
|
+
*/
|
|
470
|
+
async cleanup() {
|
|
471
|
+
const response = await this._fetchWithRetry(`${this.apiUrl}/admin/cleanup`, {
|
|
472
|
+
method: 'POST',
|
|
473
|
+
headers: this._headers(),
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
return response.json();
|
|
477
|
+
}
|
|
478
|
+
}
|
package/lib/session/metadata.js
CHANGED
|
@@ -4,28 +4,51 @@
|
|
|
4
4
|
* Extracts project information, git status, and other context
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { execFileSync } from 'child_process';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { basename, join } from 'path';
|
|
9
10
|
import { homedir, hostname, userInfo } from 'os';
|
|
10
|
-
import {
|
|
11
|
+
import { readFile } from 'fs/promises';
|
|
12
|
+
|
|
13
|
+
function getGitEnv() {
|
|
14
|
+
const env = { ...process.env };
|
|
15
|
+
// Prevent ambient git env vars (from other tests/processes) from changing behavior.
|
|
16
|
+
delete env.GIT_DIR;
|
|
17
|
+
delete env.GIT_WORK_TREE;
|
|
18
|
+
delete env.GIT_COMMON_DIR;
|
|
19
|
+
delete env.GIT_INDEX_FILE;
|
|
20
|
+
delete env.GIT_OBJECT_DIRECTORY;
|
|
21
|
+
delete env.GIT_ALTERNATE_OBJECT_DIRECTORIES;
|
|
22
|
+
delete env.GIT_CEILING_DIRECTORIES;
|
|
23
|
+
return env;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function gitExec(cwd, args) {
|
|
27
|
+
return execFileSync('git', args, {
|
|
28
|
+
cwd,
|
|
29
|
+
env: getGitEnv(),
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
32
|
+
}).trim();
|
|
33
|
+
}
|
|
11
34
|
|
|
12
35
|
/**
|
|
13
36
|
* Extract project name from git repository or fall back to directory name
|
|
14
37
|
*/
|
|
15
38
|
export async function getProjectName(cwd) {
|
|
16
39
|
try {
|
|
40
|
+
if (!isGitRepo(cwd)) {
|
|
41
|
+
return basename(cwd);
|
|
42
|
+
}
|
|
43
|
+
|
|
17
44
|
// Try to get git remote URL
|
|
18
|
-
const gitRemote =
|
|
19
|
-
cwd,
|
|
20
|
-
encoding: 'utf8',
|
|
21
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
22
|
-
}).trim();
|
|
45
|
+
const gitRemote = gitExec(cwd, ['config', '--get', 'remote.origin.url']);
|
|
23
46
|
|
|
24
47
|
if (gitRemote) {
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
48
|
+
const cleaned = gitRemote.replace(/\/+$/, '');
|
|
49
|
+
const lastPart = cleaned.split(/[\/:]/).pop();
|
|
50
|
+
if (lastPart) {
|
|
51
|
+
return lastPart.replace(/\.git$/, '');
|
|
29
52
|
}
|
|
30
53
|
}
|
|
31
54
|
} catch (e) {
|
|
@@ -41,11 +64,10 @@ export async function getProjectName(cwd) {
|
|
|
41
64
|
*/
|
|
42
65
|
export function getCurrentBranch(cwd) {
|
|
43
66
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}).trim();
|
|
67
|
+
if (!isGitRepo(cwd)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const branch = gitExec(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
49
71
|
return branch || null;
|
|
50
72
|
} catch (e) {
|
|
51
73
|
return null;
|
|
@@ -57,11 +79,10 @@ export function getCurrentBranch(cwd) {
|
|
|
57
79
|
*/
|
|
58
80
|
export function getCommitHash(cwd) {
|
|
59
81
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}).trim();
|
|
82
|
+
if (!isGitRepo(cwd)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const hash = gitExec(cwd, ['rev-parse', '--short', 'HEAD']);
|
|
65
86
|
return hash || null;
|
|
66
87
|
} catch (e) {
|
|
67
88
|
return null;
|
|
@@ -73,12 +94,12 @@ export function getCommitHash(cwd) {
|
|
|
73
94
|
*/
|
|
74
95
|
export function getLastEditedFile(cwd) {
|
|
75
96
|
try {
|
|
97
|
+
if (!isGitRepo(cwd)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
76
101
|
// Get modified files from git status
|
|
77
|
-
const status =
|
|
78
|
-
cwd,
|
|
79
|
-
encoding: 'utf8',
|
|
80
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
81
|
-
}).trim();
|
|
102
|
+
const status = gitExec(cwd, ['status', '--porcelain']);
|
|
82
103
|
|
|
83
104
|
if (status) {
|
|
84
105
|
// Get the first modified file
|
|
@@ -106,15 +127,25 @@ export function getLastEditedFile(cwd) {
|
|
|
106
127
|
}
|
|
107
128
|
}
|
|
108
129
|
|
|
109
|
-
// Try to get the most recently modified file from git
|
|
110
|
-
|
|
111
|
-
cwd,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
130
|
+
// Try to get the most recently modified file from git diff
|
|
131
|
+
try {
|
|
132
|
+
const diffNames = gitExec(cwd, ['diff', '--name-only', 'HEAD~1', 'HEAD']);
|
|
133
|
+
const first = diffNames.split('\n').find(Boolean);
|
|
134
|
+
if (first) {
|
|
135
|
+
return first.trim();
|
|
136
|
+
}
|
|
137
|
+
} catch (_) {
|
|
138
|
+
// Ignore
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Fallback: any modified tracked file
|
|
142
|
+
try {
|
|
143
|
+
const modified = gitExec(cwd, ['ls-files', '-m']);
|
|
144
|
+
const first = modified.split('\n').find(Boolean);
|
|
145
|
+
return first ? first.trim() : null;
|
|
146
|
+
} catch (_) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
118
149
|
} catch (e) {
|
|
119
150
|
return null;
|
|
120
151
|
}
|
|
@@ -125,19 +156,19 @@ export function getLastEditedFile(cwd) {
|
|
|
125
156
|
*/
|
|
126
157
|
export function getRecentCommits(cwd, count = 3) {
|
|
127
158
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
159
|
+
if (!isGitRepo(cwd)) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const log = gitExec(cwd, ['log', '-n', String(count), '--pretty=format:%h%x1f%s']);
|
|
133
164
|
|
|
134
165
|
if (!log) return [];
|
|
135
166
|
|
|
136
167
|
return log.split('\n').map(line => {
|
|
137
|
-
const [hash, ...messageParts] = line.split('
|
|
168
|
+
const [hash, ...messageParts] = line.split('\x1f');
|
|
138
169
|
return {
|
|
139
170
|
hash: hash || '',
|
|
140
|
-
message: messageParts.join('
|
|
171
|
+
message: messageParts.join('\x1f') || ''
|
|
141
172
|
};
|
|
142
173
|
});
|
|
143
174
|
} catch (e) {
|
|
@@ -176,12 +207,12 @@ export function getCurrentTask(cwd) {
|
|
|
176
207
|
*/
|
|
177
208
|
export function isGitRepo(cwd) {
|
|
178
209
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return true;
|
|
210
|
+
// Avoid discovering a parent repo by requiring a .git marker in this directory.
|
|
211
|
+
if (!existsSync(join(cwd, '.git'))) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const inside = gitExec(cwd, ['rev-parse', '--is-inside-work-tree']);
|
|
215
|
+
return inside === 'true';
|
|
185
216
|
} catch (e) {
|
|
186
217
|
return false;
|
|
187
218
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
// Simple in-memory cache for mute status
|
|
8
8
|
// Key: session_id, Value: { muted: boolean, timestamp: number }
|
|
9
|
-
const
|
|
9
|
+
const MUTE_CACHE_KEY = Symbol.for('teleportation.session.muteCache');
|
|
10
|
+
const muteCache = globalThis[MUTE_CACHE_KEY] || (globalThis[MUTE_CACHE_KEY] = new Map());
|
|
10
11
|
const CACHE_TTL = 60000; // 1 minute cache TTL
|
|
11
12
|
|
|
12
13
|
/**
|