@hypertabai/mcp 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +264 -0
- package/dist/bridge/api-client.d.ts +157 -0
- package/dist/bridge/api-client.d.ts.map +1 -0
- package/dist/bridge/api-client.js +140 -0
- package/dist/bridge/api-client.js.map +1 -0
- package/dist/bridge/artifacts.d.ts +11 -0
- package/dist/bridge/artifacts.d.ts.map +1 -0
- package/dist/bridge/artifacts.js +45 -0
- package/dist/bridge/artifacts.js.map +1 -0
- package/dist/bridge/command-contract.d.ts +14 -0
- package/dist/bridge/command-contract.d.ts.map +1 -0
- package/dist/bridge/command-contract.js +152 -0
- package/dist/bridge/command-contract.js.map +1 -0
- package/dist/bridge/index.d.ts +248 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +875 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/bridge/spool.d.ts +30 -0
- package/dist/bridge/spool.d.ts.map +1 -0
- package/dist/bridge/spool.js +108 -0
- package/dist/bridge/spool.js.map +1 -0
- package/dist/connect.js +1 -1
- package/dist/idempotency.d.ts +11 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +63 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/install/skill-writer.d.ts +1 -1
- package/dist/install/skill-writer.d.ts.map +1 -1
- package/dist/install/skill-writer.js +21 -1
- package/dist/install/skill-writer.js.map +1 -1
- package/dist/surfaces/build.d.ts +22 -0
- package/dist/surfaces/build.d.ts.map +1 -0
- package/dist/surfaces/build.js +122 -0
- package/dist/surfaces/build.js.map +1 -0
- package/dist/surfaces/detect.d.ts +20 -0
- package/dist/surfaces/detect.d.ts.map +1 -0
- package/dist/surfaces/detect.js +130 -0
- package/dist/surfaces/detect.js.map +1 -0
- package/dist/surfaces/index.d.ts +35 -0
- package/dist/surfaces/index.d.ts.map +1 -0
- package/dist/surfaces/index.js +117 -0
- package/dist/surfaces/index.js.map +1 -0
- package/dist/surfaces/package.d.ts +48 -0
- package/dist/surfaces/package.d.ts.map +1 -0
- package/dist/surfaces/package.js +86 -0
- package/dist/surfaces/package.js.map +1 -0
- package/dist/surfaces/publish-client.d.ts +40 -0
- package/dist/surfaces/publish-client.d.ts.map +1 -0
- package/dist/surfaces/publish-client.js +111 -0
- package/dist/surfaces/publish-client.js.map +1 -0
- package/package.json +8 -3
- package/templates/bridge-wrapper.mjs +138 -0
- package/templates/bridge-wrapper.sh +78 -0
- package/dist/__tests__/config-writer.test.d.ts +0 -2
- package/dist/__tests__/config-writer.test.d.ts.map +0 -1
- package/dist/__tests__/config-writer.test.js +0 -186
- package/dist/__tests__/config-writer.test.js.map +0 -1
- package/dist/__tests__/index.test.d.ts +0 -2
- package/dist/__tests__/index.test.d.ts.map +0 -1
- package/dist/__tests__/index.test.js +0 -113
- package/dist/__tests__/index.test.js.map +0 -1
- package/dist/__tests__/install.test.d.ts +0 -2
- package/dist/__tests__/install.test.d.ts.map +0 -1
- package/dist/__tests__/install.test.js +0 -211
- package/dist/__tests__/install.test.js.map +0 -1
- package/dist/__tests__/platforms.test.d.ts +0 -2
- package/dist/__tests__/platforms.test.d.ts.map +0 -1
- package/dist/__tests__/platforms.test.js +0 -109
- package/dist/__tests__/platforms.test.js.map +0 -1
- package/dist/__tests__/uninstall.test.d.ts +0 -2
- package/dist/__tests__/uninstall.test.d.ts.map +0 -1
- package/dist/__tests__/uninstall.test.js +0 -117
- package/dist/__tests__/uninstall.test.js.map +0 -1
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
import { access, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { createBridgeApiClient, DEFAULT_BRIDGE_TIMEOUT_MS, } from './api-client.js';
|
|
5
|
+
import { prepareBridgeArtifactUpload } from './artifacts.js';
|
|
6
|
+
import { defaultBridgeSpoolFile, enqueueCommandResult, enqueueEventBatch, flushBridgeSpool, readBridgeSpool, shouldSpoolBridgeFailure, } from './spool.js';
|
|
7
|
+
import { classifyBridgeCommandContract } from './command-contract.js';
|
|
8
|
+
export const DEFAULT_BRIDGE_API_URL = 'https://api.hypertab.ai';
|
|
9
|
+
export const BRIDGE_CLIENT_TYPES = ['codex', 'claude_code', 'cursor', 'browser', 'cloud', 'other'];
|
|
10
|
+
export function registerBridgeCommand(program, deps = {}) {
|
|
11
|
+
const runStart = deps.runStart ?? runBridgeDaemon;
|
|
12
|
+
const runResult = deps.runResult ?? runBridgeResult;
|
|
13
|
+
const runStatus = deps.runStatus ?? runBridgeStatus;
|
|
14
|
+
const runTokenCreate = deps.runTokenCreate ?? runBridgeTokenCreate;
|
|
15
|
+
const runEventBatch = deps.runEventBatch ?? runBridgeEventBatch;
|
|
16
|
+
const runFlush = deps.runFlush ?? runBridgeFlush;
|
|
17
|
+
const runArtifactUpload = deps.runArtifactUpload ?? runBridgeArtifactUpload;
|
|
18
|
+
const runWrapperInit = deps.runWrapperInit ?? runBridgeWrapperInit;
|
|
19
|
+
const runWrapperDoctor = deps.runWrapperDoctor ?? runBridgeWrapperDoctor;
|
|
20
|
+
const exit = deps.exit ?? ((code) => process.exit(code));
|
|
21
|
+
const bridge = program.command('bridge').description('Run the local Hypertab bridge for agent activity');
|
|
22
|
+
bridge
|
|
23
|
+
.command('start')
|
|
24
|
+
.description('Heartbeat a local agent client and claim cloud commands')
|
|
25
|
+
.option('-k, --api-key <key>', 'Hypertab bridge/API key. Prefer HYPERTAB_API_KEY env var for safety.')
|
|
26
|
+
.option('-u, --api-url <url>', 'Hypertab API URL', DEFAULT_BRIDGE_API_URL)
|
|
27
|
+
.option('--client-name <name>', 'Stable local client name. Defaults to the machine hostname.')
|
|
28
|
+
.option('--client-type <type>', `Client type: ${BRIDGE_CLIENT_TYPES.join(' | ')}`, 'codex')
|
|
29
|
+
.option('--heartbeat-interval-ms <ms>', 'Heartbeat interval for long-running mode', parsePositiveInt, 30_000)
|
|
30
|
+
.option('--command-interval-ms <ms>', 'Command claim interval for long-running mode', parsePositiveInt, 5_000)
|
|
31
|
+
.option('--command-limit <n>', 'Max commands claimed per poll', parsePositiveInt, 10)
|
|
32
|
+
.option('--session', 'Start a durable agent session after heartbeat')
|
|
33
|
+
.option('--objective <text>', 'Create a durable run for this objective')
|
|
34
|
+
.option('--run-title <title>', 'Run title when --objective is provided')
|
|
35
|
+
.option('--source <source>', 'Run source label', 'cli_bridge')
|
|
36
|
+
.option('--once', 'Run one heartbeat/claim cycle and exit')
|
|
37
|
+
.option('--json', 'Print machine-readable JSON lines')
|
|
38
|
+
.option('--spool-file <path>', 'Local JSONL spool file', defaultBridgeSpoolFile())
|
|
39
|
+
.option('--no-spool', 'Disable local event/result spooling')
|
|
40
|
+
.option('--timeout-ms <ms>', 'Bridge API request timeout in milliseconds', parsePositiveInt, DEFAULT_BRIDGE_TIMEOUT_MS)
|
|
41
|
+
.action(async (opts) => {
|
|
42
|
+
const code = await runStart(normalizeBridgeStartOptions(opts));
|
|
43
|
+
exit(code);
|
|
44
|
+
});
|
|
45
|
+
bridge
|
|
46
|
+
.command('status')
|
|
47
|
+
.description('Show local bridge freshness, attention, and command delivery health')
|
|
48
|
+
.option('-k, --api-key <key>', 'Hypertab bridge/API key. Prefer HYPERTAB_API_KEY env var for safety.')
|
|
49
|
+
.option('-u, --api-url <url>', 'Hypertab API URL', DEFAULT_BRIDGE_API_URL)
|
|
50
|
+
.option('--client-id <id>', 'Optional client id for broad bridge/API keys. Bridge-client tokens are already scoped.')
|
|
51
|
+
.option('--limit <n>', 'Max clients/runs/commands to inspect', parsePositiveInt, 50)
|
|
52
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
53
|
+
.option('--timeout-ms <ms>', 'Bridge API request timeout in milliseconds', parsePositiveInt, DEFAULT_BRIDGE_TIMEOUT_MS)
|
|
54
|
+
.action(async (opts) => {
|
|
55
|
+
const code = await runStatus(normalizeBridgeStatusOptions(opts));
|
|
56
|
+
exit(code);
|
|
57
|
+
});
|
|
58
|
+
const token = bridge.command('token').description('Manage local bridge credentials');
|
|
59
|
+
token
|
|
60
|
+
.command('create')
|
|
61
|
+
.description('Mint a client-bound local bridge token')
|
|
62
|
+
.option('-k, --api-key <key>', 'Admin Hypertab API key. Prefer HYPERTAB_API_KEY env var for safety.')
|
|
63
|
+
.option('-u, --api-url <url>', 'Hypertab API URL', DEFAULT_BRIDGE_API_URL)
|
|
64
|
+
.option('--name <name>', 'Token display name. Defaults to "<client-name> bridge".')
|
|
65
|
+
.requiredOption('--client-name <name>', 'Stable local client name bound into the token')
|
|
66
|
+
.option('--client-type <type>', `Client type: ${BRIDGE_CLIENT_TYPES.join(' | ')}`, 'codex')
|
|
67
|
+
.option('--expires-in-days <days>', 'Token lifetime in days, 1-365', parsePositiveInt, 30)
|
|
68
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
69
|
+
.option('--timeout-ms <ms>', 'Bridge API request timeout in milliseconds', parsePositiveInt, DEFAULT_BRIDGE_TIMEOUT_MS)
|
|
70
|
+
.action(async (opts) => {
|
|
71
|
+
const code = await runTokenCreate(normalizeBridgeTokenCreateOptions(opts));
|
|
72
|
+
exit(code);
|
|
73
|
+
});
|
|
74
|
+
bridge
|
|
75
|
+
.command('result')
|
|
76
|
+
.description('Report the result of a delivered cloud command')
|
|
77
|
+
.option('-k, --api-key <key>', 'Hypertab bridge/API key. Prefer HYPERTAB_API_KEY env var for safety.')
|
|
78
|
+
.option('-u, --api-url <url>', 'Hypertab API URL', DEFAULT_BRIDGE_API_URL)
|
|
79
|
+
.requiredOption('--command-id <id>', 'Delivered command id')
|
|
80
|
+
.option('--client-id <id>', 'Client id for broad bridge tokens. Bridge-client tokens can omit it.')
|
|
81
|
+
.requiredOption('--status <status>', 'completed | failed | cancelled')
|
|
82
|
+
.option('--error-message <message>', 'Failure/cancellation details')
|
|
83
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
84
|
+
.option('--spool-file <path>', 'Local JSONL spool file', defaultBridgeSpoolFile())
|
|
85
|
+
.option('--no-spool', 'Disable local result spooling on retryable failures')
|
|
86
|
+
.option('--timeout-ms <ms>', 'Bridge API request timeout in milliseconds', parsePositiveInt, DEFAULT_BRIDGE_TIMEOUT_MS)
|
|
87
|
+
.action(async (opts) => {
|
|
88
|
+
const code = await runResult(normalizeBridgeResultOptions(opts));
|
|
89
|
+
exit(code);
|
|
90
|
+
});
|
|
91
|
+
const event = bridge.command('event').description('Send local bridge progress events');
|
|
92
|
+
event
|
|
93
|
+
.command('batch')
|
|
94
|
+
.description('Post a JSON batch of local progress events')
|
|
95
|
+
.option('-k, --api-key <key>', 'Hypertab bridge/API key. Prefer HYPERTAB_API_KEY env var for safety.')
|
|
96
|
+
.option('-u, --api-url <url>', 'Hypertab API URL', DEFAULT_BRIDGE_API_URL)
|
|
97
|
+
.requiredOption('--file <path>', 'JSON file containing { "events": [...] } or an events array')
|
|
98
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
99
|
+
.option('--spool-file <path>', 'Local JSONL spool file', defaultBridgeSpoolFile())
|
|
100
|
+
.option('--no-spool', 'Disable local event spooling on retryable failures')
|
|
101
|
+
.option('--timeout-ms <ms>', 'Bridge API request timeout in milliseconds', parsePositiveInt, DEFAULT_BRIDGE_TIMEOUT_MS)
|
|
102
|
+
.action(async (opts) => {
|
|
103
|
+
const code = await runEventBatch(normalizeBridgeEventBatchOptions(opts));
|
|
104
|
+
exit(code);
|
|
105
|
+
});
|
|
106
|
+
bridge
|
|
107
|
+
.command('flush')
|
|
108
|
+
.description('Flush queued local bridge spool records')
|
|
109
|
+
.option('-k, --api-key <key>', 'Hypertab bridge/API key. Prefer HYPERTAB_API_KEY env var for safety.')
|
|
110
|
+
.option('-u, --api-url <url>', 'Hypertab API URL', DEFAULT_BRIDGE_API_URL)
|
|
111
|
+
.option('--spool-file <path>', 'Local JSONL spool file', defaultBridgeSpoolFile())
|
|
112
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
113
|
+
.option('--timeout-ms <ms>', 'Bridge API request timeout in milliseconds', parsePositiveInt, DEFAULT_BRIDGE_TIMEOUT_MS)
|
|
114
|
+
.action(async (opts) => {
|
|
115
|
+
const code = await runFlush(normalizeBridgeFlushOptions(opts));
|
|
116
|
+
exit(code);
|
|
117
|
+
});
|
|
118
|
+
const artifact = bridge.command('artifact').description('Upload local bridge artifacts to Hypertab');
|
|
119
|
+
artifact
|
|
120
|
+
.command('upload')
|
|
121
|
+
.description('Upload a local file as an agent artifact')
|
|
122
|
+
.option('-k, --api-key <key>', 'Hypertab bridge/API key. Prefer HYPERTAB_API_KEY env var for safety.')
|
|
123
|
+
.option('-u, --api-url <url>', 'Hypertab API URL', DEFAULT_BRIDGE_API_URL)
|
|
124
|
+
.requiredOption('--file <path>', 'Local file to upload')
|
|
125
|
+
.option('--run-id <id>', 'Agent run id to attach the artifact to')
|
|
126
|
+
.option('--name <name>', 'Artifact display name. Defaults to the file basename.')
|
|
127
|
+
.option('--artifact-type <type>', 'Artifact type, for example screenshot, report, log, dataset, file')
|
|
128
|
+
.option('--mime-type <type>', 'Override MIME type')
|
|
129
|
+
.option('--metadata <json>', 'Metadata JSON object')
|
|
130
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
131
|
+
.option('--timeout-ms <ms>', 'Bridge API request timeout in milliseconds', parsePositiveInt, DEFAULT_BRIDGE_TIMEOUT_MS)
|
|
132
|
+
.action(async (opts) => {
|
|
133
|
+
const code = await runArtifactUpload(normalizeBridgeArtifactUploadOptions(opts));
|
|
134
|
+
exit(code);
|
|
135
|
+
});
|
|
136
|
+
const wrapper = bridge.command('wrapper').description('Create local bridge wrapper starter files');
|
|
137
|
+
wrapper
|
|
138
|
+
.command('init')
|
|
139
|
+
.description('Copy a safe local bridge wrapper starter into this workspace')
|
|
140
|
+
.option('--type <type>', 'Starter type: node | shell', 'node')
|
|
141
|
+
.option('--out <path>', 'Output path. Defaults to .hypertab/bridge-wrapper.mjs or .hypertab/bridge-wrapper.sh')
|
|
142
|
+
.option('--force', 'Overwrite the output file if it already exists')
|
|
143
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
144
|
+
.action(async (opts) => {
|
|
145
|
+
const code = await runWrapperInit(normalizeBridgeWrapperInitOptions(opts));
|
|
146
|
+
exit(code);
|
|
147
|
+
});
|
|
148
|
+
wrapper
|
|
149
|
+
.command('doctor')
|
|
150
|
+
.description('Check local bridge wrapper readiness without contacting Hypertab')
|
|
151
|
+
.option('--path <path>', 'Wrapper path to inspect', '.hypertab/bridge-wrapper.mjs')
|
|
152
|
+
.option('-k, --api-key <key>', 'Hypertab bridge/API key. Prefer HYPERTAB_API_KEY env var for safety.')
|
|
153
|
+
.option('--spool-file <path>', 'Local JSONL spool file', defaultBridgeSpoolFile())
|
|
154
|
+
.option('--json', 'Print machine-readable JSON output')
|
|
155
|
+
.action(async (opts) => {
|
|
156
|
+
const code = await runWrapperDoctor(normalizeBridgeWrapperDoctorOptions(opts));
|
|
157
|
+
exit(code);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
export function normalizeBridgeStartOptions(opts) {
|
|
161
|
+
return {
|
|
162
|
+
apiKey: opts.apiKey,
|
|
163
|
+
apiUrl: opts.apiUrl,
|
|
164
|
+
clientName: opts.clientName,
|
|
165
|
+
clientType: normalizeBridgeClientType(opts.clientType),
|
|
166
|
+
heartbeatIntervalMs: opts.heartbeatIntervalMs,
|
|
167
|
+
commandIntervalMs: opts.commandIntervalMs,
|
|
168
|
+
commandLimit: Math.min(opts.commandLimit, 50),
|
|
169
|
+
session: opts.session ?? false,
|
|
170
|
+
objective: opts.objective,
|
|
171
|
+
runTitle: opts.runTitle,
|
|
172
|
+
source: opts.source,
|
|
173
|
+
once: opts.once ?? false,
|
|
174
|
+
json: opts.json ?? false,
|
|
175
|
+
spool: opts.spool ?? true,
|
|
176
|
+
spoolFile: opts.spoolFile ?? defaultBridgeSpoolFile(),
|
|
177
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export function normalizeBridgeResultOptions(opts) {
|
|
181
|
+
return {
|
|
182
|
+
apiKey: opts.apiKey,
|
|
183
|
+
apiUrl: opts.apiUrl,
|
|
184
|
+
commandId: opts.commandId,
|
|
185
|
+
clientId: opts.clientId,
|
|
186
|
+
status: normalizeCommandResultStatus(opts.status),
|
|
187
|
+
errorMessage: opts.errorMessage,
|
|
188
|
+
json: opts.json ?? false,
|
|
189
|
+
spool: opts.spool ?? true,
|
|
190
|
+
spoolFile: opts.spoolFile ?? defaultBridgeSpoolFile(),
|
|
191
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export function normalizeBridgeStatusOptions(opts) {
|
|
195
|
+
return {
|
|
196
|
+
apiKey: opts.apiKey,
|
|
197
|
+
apiUrl: opts.apiUrl,
|
|
198
|
+
clientId: opts.clientId,
|
|
199
|
+
limit: Math.min(opts.limit, 100),
|
|
200
|
+
json: opts.json ?? false,
|
|
201
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export function normalizeBridgeTokenCreateOptions(opts) {
|
|
205
|
+
const clientName = opts.clientName?.trim();
|
|
206
|
+
if (!clientName)
|
|
207
|
+
throw new Error('Bridge token client name is required.');
|
|
208
|
+
const expiresInDays = opts.expiresInDays ?? 30;
|
|
209
|
+
if (expiresInDays > 365)
|
|
210
|
+
throw new Error('Bridge token expiry must be between 1 and 365 days.');
|
|
211
|
+
return {
|
|
212
|
+
apiKey: opts.apiKey,
|
|
213
|
+
apiUrl: opts.apiUrl,
|
|
214
|
+
name: optionalTrimmedString(opts.name),
|
|
215
|
+
clientName,
|
|
216
|
+
clientType: normalizeBridgeClientType(opts.clientType ?? 'codex'),
|
|
217
|
+
expiresInDays,
|
|
218
|
+
json: opts.json ?? false,
|
|
219
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export function normalizeBridgeEventBatchOptions(opts) {
|
|
223
|
+
return {
|
|
224
|
+
apiKey: opts.apiKey,
|
|
225
|
+
apiUrl: opts.apiUrl,
|
|
226
|
+
file: opts.file,
|
|
227
|
+
json: opts.json ?? false,
|
|
228
|
+
spool: opts.spool ?? true,
|
|
229
|
+
spoolFile: opts.spoolFile ?? defaultBridgeSpoolFile(),
|
|
230
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
export function normalizeBridgeFlushOptions(opts) {
|
|
234
|
+
return {
|
|
235
|
+
apiKey: opts.apiKey,
|
|
236
|
+
apiUrl: opts.apiUrl,
|
|
237
|
+
spoolFile: opts.spoolFile ?? defaultBridgeSpoolFile(),
|
|
238
|
+
json: opts.json ?? false,
|
|
239
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
export function normalizeBridgeArtifactUploadOptions(opts) {
|
|
243
|
+
return {
|
|
244
|
+
apiKey: opts.apiKey,
|
|
245
|
+
apiUrl: opts.apiUrl,
|
|
246
|
+
file: opts.file,
|
|
247
|
+
runId: opts.runId,
|
|
248
|
+
name: opts.name,
|
|
249
|
+
artifactType: opts.artifactType,
|
|
250
|
+
mimeType: opts.mimeType,
|
|
251
|
+
metadata: opts.metadata,
|
|
252
|
+
json: opts.json ?? false,
|
|
253
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
export function normalizeBridgeWrapperInitOptions(opts) {
|
|
257
|
+
return {
|
|
258
|
+
type: normalizeWrapperType(opts.type ?? 'node'),
|
|
259
|
+
out: opts.out,
|
|
260
|
+
force: opts.force ?? false,
|
|
261
|
+
json: opts.json ?? false,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
export function normalizeBridgeWrapperDoctorOptions(opts) {
|
|
265
|
+
return {
|
|
266
|
+
path: opts.path,
|
|
267
|
+
apiKey: opts.apiKey,
|
|
268
|
+
spoolFile: opts.spoolFile ?? defaultBridgeSpoolFile(),
|
|
269
|
+
json: opts.json ?? false,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export function resolveBridgeApiKey(input) {
|
|
273
|
+
const key = input.apiKey ?? input.env?.HYPERTAB_API_KEY ?? process.env.HYPERTAB_API_KEY;
|
|
274
|
+
return key?.startsWith('ht_sk_') ? key : null;
|
|
275
|
+
}
|
|
276
|
+
export async function runBridgeDaemon(options) {
|
|
277
|
+
const stdout = options.stdout ?? process.stdout;
|
|
278
|
+
const apiKey = resolveBridgeApiKey({ apiKey: options.apiKey });
|
|
279
|
+
if (!apiKey) {
|
|
280
|
+
writeFailure(options, 'BRIDGE_API_KEY_MISSING', 'Provide a Hypertab API key with --api-key or HYPERTAB_API_KEY.');
|
|
281
|
+
return 2;
|
|
282
|
+
}
|
|
283
|
+
const client = createBridgeApiClient({ apiUrl: options.apiUrl, apiKey, timeoutMs: options.timeoutMs, fetchImpl: options.fetchImpl });
|
|
284
|
+
const context = await initializeBridge(client, options);
|
|
285
|
+
if (!context.ok) {
|
|
286
|
+
writeFailure(options, context.code, context.message, context.details);
|
|
287
|
+
return 2;
|
|
288
|
+
}
|
|
289
|
+
writeBridgeStatus(stdout, options, context.data);
|
|
290
|
+
if (options.spool)
|
|
291
|
+
await writeSpoolFlushStatus(stdout, options, await flushBridgeSpool(client, options.spoolFile));
|
|
292
|
+
let iterations = 0;
|
|
293
|
+
let lastHeartbeat = Date.now();
|
|
294
|
+
while (true) {
|
|
295
|
+
if (iterations > 0 && Date.now() - lastHeartbeat >= options.heartbeatIntervalMs) {
|
|
296
|
+
const heartbeat = await sendHeartbeat(client, options);
|
|
297
|
+
if (!heartbeat.ok) {
|
|
298
|
+
writeFailure(options, heartbeat.code, heartbeat.message, heartbeat.details);
|
|
299
|
+
return 2;
|
|
300
|
+
}
|
|
301
|
+
lastHeartbeat = Date.now();
|
|
302
|
+
}
|
|
303
|
+
const claim = await client.claimCommands({
|
|
304
|
+
client_id: context.data.client.id,
|
|
305
|
+
limit: options.commandLimit,
|
|
306
|
+
});
|
|
307
|
+
if (!claim.ok) {
|
|
308
|
+
writeFailure(options, claim.error.code, claim.error.message, claim.error.details);
|
|
309
|
+
return 2;
|
|
310
|
+
}
|
|
311
|
+
for (const command of claim.data.commands)
|
|
312
|
+
writeCommand(stdout, options, command);
|
|
313
|
+
if (options.spool)
|
|
314
|
+
await writeSpoolFlushStatus(stdout, options, await flushBridgeSpool(client, options.spoolFile));
|
|
315
|
+
iterations += 1;
|
|
316
|
+
if (options.once || (options.maxIterations !== undefined && iterations >= options.maxIterations))
|
|
317
|
+
break;
|
|
318
|
+
await (options.sleep ?? sleep)(options.commandIntervalMs);
|
|
319
|
+
}
|
|
320
|
+
if (!options.json && options.once)
|
|
321
|
+
stdout.write('Bridge cycle complete.\n');
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
export async function runBridgeResult(options) {
|
|
325
|
+
const stdout = options.stdout ?? process.stdout;
|
|
326
|
+
const apiKey = resolveBridgeApiKey({ apiKey: options.apiKey });
|
|
327
|
+
if (!apiKey) {
|
|
328
|
+
writeFailure(options, 'BRIDGE_API_KEY_MISSING', 'Provide a Hypertab API key with --api-key or HYPERTAB_API_KEY.');
|
|
329
|
+
return 2;
|
|
330
|
+
}
|
|
331
|
+
const client = createBridgeApiClient({ apiUrl: options.apiUrl, apiKey, timeoutMs: options.timeoutMs, fetchImpl: options.fetchImpl });
|
|
332
|
+
const result = await client.reportCommandResult({
|
|
333
|
+
command_id: options.commandId,
|
|
334
|
+
client_id: options.clientId,
|
|
335
|
+
status: options.status,
|
|
336
|
+
error_message: options.errorMessage,
|
|
337
|
+
});
|
|
338
|
+
if (!result.ok) {
|
|
339
|
+
if (options.spool && shouldSpoolBridgeFailure(result.status)) {
|
|
340
|
+
await enqueueCommandResult(options.spoolFile, {
|
|
341
|
+
command_id: options.commandId,
|
|
342
|
+
client_id: options.clientId,
|
|
343
|
+
status: options.status,
|
|
344
|
+
error_message: options.errorMessage,
|
|
345
|
+
});
|
|
346
|
+
writeSpoolQueued(stdout, options, 'command_result', options.spoolFile);
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
writeFailure(options, result.error.code, result.error.message, result.error.details);
|
|
350
|
+
return 2;
|
|
351
|
+
}
|
|
352
|
+
if (options.json) {
|
|
353
|
+
stdout.write(`${JSON.stringify({ success: true, data: result.data })}\n`);
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
stdout.write(`Reported command ${result.data.command.id} as ${result.data.command.status ?? options.status}.\n`);
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
export async function runBridgeStatus(options) {
|
|
360
|
+
const stdout = options.stdout ?? process.stdout;
|
|
361
|
+
const apiKey = resolveBridgeApiKey({ apiKey: options.apiKey });
|
|
362
|
+
if (!apiKey) {
|
|
363
|
+
writeFailure(options, 'BRIDGE_API_KEY_MISSING', 'Provide a Hypertab API key with --api-key or HYPERTAB_API_KEY.');
|
|
364
|
+
return 2;
|
|
365
|
+
}
|
|
366
|
+
const client = createBridgeApiClient({ apiUrl: options.apiUrl, apiKey, timeoutMs: options.timeoutMs, fetchImpl: options.fetchImpl });
|
|
367
|
+
const result = await client.getBridgeHealth({
|
|
368
|
+
client_id: options.clientId,
|
|
369
|
+
limit: options.limit,
|
|
370
|
+
});
|
|
371
|
+
if (!result.ok) {
|
|
372
|
+
writeFailure(options, result.error.code, result.error.message, result.error.details);
|
|
373
|
+
return 2;
|
|
374
|
+
}
|
|
375
|
+
writeBridgeHealthStatus(stdout, options, result.data.bridge_health);
|
|
376
|
+
return 0;
|
|
377
|
+
}
|
|
378
|
+
export async function runBridgeTokenCreate(options) {
|
|
379
|
+
const stdout = options.stdout ?? process.stdout;
|
|
380
|
+
const apiKey = resolveBridgeApiKey({ apiKey: options.apiKey });
|
|
381
|
+
if (!apiKey) {
|
|
382
|
+
writeFailure(options, 'BRIDGE_API_KEY_MISSING', 'Provide an admin Hypertab API key with --api-key or HYPERTAB_API_KEY to mint a bridge token.');
|
|
383
|
+
return 2;
|
|
384
|
+
}
|
|
385
|
+
const client = createBridgeApiClient({ apiUrl: options.apiUrl, apiKey, timeoutMs: options.timeoutMs, fetchImpl: options.fetchImpl });
|
|
386
|
+
const result = await client.createBridgeToken({
|
|
387
|
+
name: options.name,
|
|
388
|
+
client_name: options.clientName,
|
|
389
|
+
client_type: options.clientType,
|
|
390
|
+
expires_in_days: options.expiresInDays,
|
|
391
|
+
metadata: { source: 'hypertab_bridge_cli' },
|
|
392
|
+
});
|
|
393
|
+
if (!result.ok) {
|
|
394
|
+
writeFailure(options, result.error.code, result.error.message, result.error.details);
|
|
395
|
+
return 2;
|
|
396
|
+
}
|
|
397
|
+
writeBridgeTokenCreateStatus(stdout, options, result.data);
|
|
398
|
+
return 0;
|
|
399
|
+
}
|
|
400
|
+
export async function runBridgeEventBatch(options) {
|
|
401
|
+
const stdout = options.stdout ?? process.stdout;
|
|
402
|
+
const apiKey = resolveBridgeApiKey({ apiKey: options.apiKey });
|
|
403
|
+
if (!apiKey) {
|
|
404
|
+
writeFailure(options, 'BRIDGE_API_KEY_MISSING', 'Provide a Hypertab API key with --api-key or HYPERTAB_API_KEY.');
|
|
405
|
+
return 2;
|
|
406
|
+
}
|
|
407
|
+
let payload;
|
|
408
|
+
try {
|
|
409
|
+
payload = await readBridgeEventBatchFile(options.file);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
writeFailure(options, 'BRIDGE_EVENT_BATCH_INVALID', error instanceof Error ? error.message : 'Failed to read bridge event batch file.');
|
|
413
|
+
return 2;
|
|
414
|
+
}
|
|
415
|
+
const client = createBridgeApiClient({ apiUrl: options.apiUrl, apiKey, timeoutMs: options.timeoutMs, fetchImpl: options.fetchImpl });
|
|
416
|
+
const result = await client.appendEventBatch(payload);
|
|
417
|
+
if (!result.ok) {
|
|
418
|
+
if (options.spool && shouldSpoolBridgeFailure(result.status)) {
|
|
419
|
+
await enqueueEventBatch(options.spoolFile, payload);
|
|
420
|
+
writeSpoolQueued(stdout, options, 'event_batch', options.spoolFile);
|
|
421
|
+
return 0;
|
|
422
|
+
}
|
|
423
|
+
writeFailure(options, result.error.code, result.error.message, result.error.details);
|
|
424
|
+
return 2;
|
|
425
|
+
}
|
|
426
|
+
writeEventBatchStatus(stdout, options, result.data);
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
export async function runBridgeFlush(options) {
|
|
430
|
+
const stdout = options.stdout ?? process.stdout;
|
|
431
|
+
const apiKey = resolveBridgeApiKey({ apiKey: options.apiKey });
|
|
432
|
+
if (!apiKey) {
|
|
433
|
+
writeFailure(options, 'BRIDGE_API_KEY_MISSING', 'Provide a Hypertab API key with --api-key or HYPERTAB_API_KEY.');
|
|
434
|
+
return 2;
|
|
435
|
+
}
|
|
436
|
+
const client = createBridgeApiClient({ apiUrl: options.apiUrl, apiKey, timeoutMs: options.timeoutMs, fetchImpl: options.fetchImpl });
|
|
437
|
+
const result = await flushBridgeSpool(client, options.spoolFile);
|
|
438
|
+
writeSpoolFlushStatus(stdout, options, result);
|
|
439
|
+
return result.remaining === 0 ? 0 : 2;
|
|
440
|
+
}
|
|
441
|
+
export async function runBridgeArtifactUpload(options) {
|
|
442
|
+
const stdout = options.stdout ?? process.stdout;
|
|
443
|
+
const apiKey = resolveBridgeApiKey({ apiKey: options.apiKey });
|
|
444
|
+
if (!apiKey) {
|
|
445
|
+
writeFailure(options, 'BRIDGE_API_KEY_MISSING', 'Provide a Hypertab API key with --api-key or HYPERTAB_API_KEY.');
|
|
446
|
+
return 2;
|
|
447
|
+
}
|
|
448
|
+
let payload;
|
|
449
|
+
try {
|
|
450
|
+
payload = await prepareBridgeArtifactUpload({
|
|
451
|
+
file: options.file,
|
|
452
|
+
runId: options.runId,
|
|
453
|
+
name: options.name,
|
|
454
|
+
type: options.artifactType,
|
|
455
|
+
mimeType: options.mimeType,
|
|
456
|
+
metadataJson: options.metadata,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
catch (error) {
|
|
460
|
+
writeFailure(options, 'BRIDGE_ARTIFACT_PREPARE_FAILED', error instanceof Error ? error.message : 'Failed to prepare artifact upload.');
|
|
461
|
+
return 2;
|
|
462
|
+
}
|
|
463
|
+
if (!options.json) {
|
|
464
|
+
stdout.write(`Uploading ${payload.name} (${formatByteSize(payload.size_bytes)}) to ${options.apiUrl}...\n`);
|
|
465
|
+
}
|
|
466
|
+
const client = createBridgeApiClient({ apiUrl: options.apiUrl, apiKey, timeoutMs: options.timeoutMs, fetchImpl: options.fetchImpl });
|
|
467
|
+
const result = await client.uploadArtifact(payload);
|
|
468
|
+
if (!result.ok) {
|
|
469
|
+
writeFailure(options, result.error.code, result.error.message, result.error.details);
|
|
470
|
+
return 2;
|
|
471
|
+
}
|
|
472
|
+
writeArtifactUploadStatus(stdout, options, result.data.artifact);
|
|
473
|
+
return 0;
|
|
474
|
+
}
|
|
475
|
+
export async function runBridgeWrapperInit(options) {
|
|
476
|
+
const stdout = options.stdout ?? process.stdout;
|
|
477
|
+
const template = wrapperTemplateName(options.type);
|
|
478
|
+
const out = options.out ?? `.hypertab/${template}`;
|
|
479
|
+
if (!options.force && (await fileExists(out))) {
|
|
480
|
+
writeFailure(options, 'BRIDGE_WRAPPER_EXISTS', `Bridge wrapper '${out}' already exists.`, 'Pass --force to overwrite it, or choose a different --out path.');
|
|
481
|
+
return 2;
|
|
482
|
+
}
|
|
483
|
+
const body = await readFile(new URL(`../../templates/${template}`, import.meta.url), 'utf8');
|
|
484
|
+
await mkdir(dirname(out), { recursive: true });
|
|
485
|
+
await writeFile(out, body, { mode: options.type === 'shell' ? 0o755 : 0o644 });
|
|
486
|
+
const data = { path: out, type: options.type, template };
|
|
487
|
+
if (options.json) {
|
|
488
|
+
stdout.write(`${JSON.stringify({ success: true, data })}\n`);
|
|
489
|
+
return 0;
|
|
490
|
+
}
|
|
491
|
+
stdout.write(`Created ${options.type} bridge wrapper at ${out}.\n`);
|
|
492
|
+
return 0;
|
|
493
|
+
}
|
|
494
|
+
export async function runBridgeWrapperDoctor(options) {
|
|
495
|
+
const stdout = options.stdout ?? process.stdout;
|
|
496
|
+
const wrapperPath = options.path ?? '.hypertab/bridge-wrapper.mjs';
|
|
497
|
+
const checks = [];
|
|
498
|
+
const wrapperType = inferWrapperType(wrapperPath);
|
|
499
|
+
let body = null;
|
|
500
|
+
try {
|
|
501
|
+
const info = await stat(wrapperPath);
|
|
502
|
+
if (info.isFile()) {
|
|
503
|
+
checks.push({ id: 'wrapper_file', status: 'pass', message: `Wrapper file exists at ${wrapperPath}.` });
|
|
504
|
+
if (wrapperType === 'shell' && (info.mode & 0o111) === 0) {
|
|
505
|
+
checks.push({
|
|
506
|
+
id: 'wrapper_executable',
|
|
507
|
+
status: 'warn',
|
|
508
|
+
message: 'Shell wrapper is not executable. Run chmod +x or invoke it with sh.',
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
body = await readFile(wrapperPath, 'utf8');
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
checks.push({ id: 'wrapper_file', status: 'fail', message: `${wrapperPath} exists but is not a file.` });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
const message = error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
|
519
|
+
? `Wrapper file is missing at ${wrapperPath}. Run hypertab bridge wrapper init first.`
|
|
520
|
+
: `Could not inspect wrapper file at ${wrapperPath}.`;
|
|
521
|
+
checks.push({ id: 'wrapper_file', status: 'fail', message });
|
|
522
|
+
}
|
|
523
|
+
const apiKey = resolveBridgeApiKey({ apiKey: options.apiKey, env: options.env });
|
|
524
|
+
checks.push(apiKey
|
|
525
|
+
? { id: 'api_key', status: 'pass', message: 'HYPERTAB_API_KEY or --api-key is available.' }
|
|
526
|
+
: {
|
|
527
|
+
id: 'api_key',
|
|
528
|
+
status: 'fail',
|
|
529
|
+
message: 'Provide a Hypertab API key with HYPERTAB_API_KEY or --api-key before running the wrapper.',
|
|
530
|
+
});
|
|
531
|
+
if (body) {
|
|
532
|
+
addStarterMarkerCheck(checks, body, 'bridge_start', 'bridge start');
|
|
533
|
+
addStarterMarkerCheck(checks, body, 'event_batch', 'bridge event batch');
|
|
534
|
+
addStarterMarkerCheck(checks, body, 'command_result', 'bridge result');
|
|
535
|
+
addStarterMarkerCheck(checks, body, 'command_contract', 'contract.allowed');
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
const records = await readBridgeSpool(options.spoolFile);
|
|
539
|
+
checks.push(records.length === 0
|
|
540
|
+
? { id: 'spool', status: 'pass', message: `No pending bridge spool records in ${options.spoolFile}.` }
|
|
541
|
+
: {
|
|
542
|
+
id: 'spool',
|
|
543
|
+
status: 'warn',
|
|
544
|
+
message: `${records.length} pending bridge spool records are waiting to flush.`,
|
|
545
|
+
details: { file: options.spoolFile, records: records.length },
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
checks.push({
|
|
550
|
+
id: 'spool',
|
|
551
|
+
status: 'warn',
|
|
552
|
+
message: `Could not read bridge spool file at ${options.spoolFile}. The wrapper can recreate it when needed.`,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
const report = {
|
|
556
|
+
path: wrapperPath,
|
|
557
|
+
type: wrapperType,
|
|
558
|
+
ready: !checks.some((check) => check.status === 'fail'),
|
|
559
|
+
spool_file: options.spoolFile,
|
|
560
|
+
checks,
|
|
561
|
+
};
|
|
562
|
+
writeBridgeWrapperDoctorStatus(stdout, options, report);
|
|
563
|
+
return report.ready ? 0 : 2;
|
|
564
|
+
}
|
|
565
|
+
async function initializeBridge(client, options) {
|
|
566
|
+
const heartbeat = await sendHeartbeat(client, options);
|
|
567
|
+
if (!heartbeat.ok)
|
|
568
|
+
return heartbeat;
|
|
569
|
+
const context = { client: heartbeat.data.client };
|
|
570
|
+
if (options.session || options.objective) {
|
|
571
|
+
const session = await client.startSession({
|
|
572
|
+
client_id: context.client.id,
|
|
573
|
+
metadata: { source: 'hypertab_bridge_cli' },
|
|
574
|
+
});
|
|
575
|
+
if (!session.ok)
|
|
576
|
+
return { ok: false, code: session.error.code, message: session.error.message, details: session.error.details };
|
|
577
|
+
context.session = session.data.session;
|
|
578
|
+
}
|
|
579
|
+
if (options.objective) {
|
|
580
|
+
const run = await client.createRun({
|
|
581
|
+
title: options.runTitle ?? defaultRunTitle(options.objective),
|
|
582
|
+
objective: options.objective,
|
|
583
|
+
client_id: context.client.id,
|
|
584
|
+
session_id: context.session?.id ?? null,
|
|
585
|
+
source: options.source,
|
|
586
|
+
metadata: { source: 'hypertab_bridge_cli' },
|
|
587
|
+
});
|
|
588
|
+
if (!run.ok)
|
|
589
|
+
return { ok: false, code: run.error.code, message: run.error.message, details: run.error.details };
|
|
590
|
+
context.run = run.data.run;
|
|
591
|
+
const eventPayload = {
|
|
592
|
+
events: [
|
|
593
|
+
{
|
|
594
|
+
run_id: context.run.id,
|
|
595
|
+
type: 'status',
|
|
596
|
+
message: 'Local bridge connected.',
|
|
597
|
+
data: { client_id: context.client.id, session_id: context.session?.id ?? null },
|
|
598
|
+
idempotency_key: `bridge-start:${context.run.id}`,
|
|
599
|
+
},
|
|
600
|
+
],
|
|
601
|
+
};
|
|
602
|
+
const event = await client.appendEventBatch(eventPayload);
|
|
603
|
+
if (!event.ok) {
|
|
604
|
+
if (options.spool && shouldSpoolBridgeFailure(event.status)) {
|
|
605
|
+
await enqueueEventBatch(options.spoolFile, eventPayload);
|
|
606
|
+
writeSpoolQueued(options.stdout ?? process.stdout, options, 'event_batch', options.spoolFile);
|
|
607
|
+
return { ok: true, data: context };
|
|
608
|
+
}
|
|
609
|
+
return { ok: false, code: event.error.code, message: event.error.message, details: event.error.details };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return { ok: true, data: context };
|
|
613
|
+
}
|
|
614
|
+
async function sendHeartbeat(client, options) {
|
|
615
|
+
const heartbeat = await client.heartbeat({
|
|
616
|
+
name: options.clientName ?? hostname(),
|
|
617
|
+
type: options.clientType,
|
|
618
|
+
metadata: {
|
|
619
|
+
source: 'hypertab_bridge_cli',
|
|
620
|
+
pid: process.pid,
|
|
621
|
+
cwd: process.cwd(),
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
if (!heartbeat.ok)
|
|
625
|
+
return { ok: false, code: heartbeat.error.code, message: heartbeat.error.message, details: heartbeat.error.details };
|
|
626
|
+
if (!heartbeat.data.client.id) {
|
|
627
|
+
return {
|
|
628
|
+
ok: false,
|
|
629
|
+
code: 'BRIDGE_CLIENT_ID_MISSING',
|
|
630
|
+
message: 'Heartbeat succeeded but the API did not return a client id.',
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
return heartbeat;
|
|
634
|
+
}
|
|
635
|
+
function writeBridgeStatus(stdout, options, context) {
|
|
636
|
+
if (options.json) {
|
|
637
|
+
stdout.write(`${JSON.stringify({ type: 'bridge_status', status: 'connected', ...context })}\n`);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
stdout.write(`Hypertab bridge connected as ${context.client.name ?? context.client.id} (${context.client.id}).\n`);
|
|
641
|
+
if (context.session)
|
|
642
|
+
stdout.write(`Session: ${context.session.id}\n`);
|
|
643
|
+
if (context.run)
|
|
644
|
+
stdout.write(`Run: ${context.run.id}\n`);
|
|
645
|
+
}
|
|
646
|
+
function writeCommand(stdout, options, command) {
|
|
647
|
+
const contract = classifyBridgeCommandContract(command);
|
|
648
|
+
if (options.json) {
|
|
649
|
+
stdout.write(`${JSON.stringify({ type: 'bridge_command', command, contract })}\n`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const suffix = contract.allowed ? 'allowed' : `rejected: ${contract.message ?? contract.reason ?? 'not allowed'}`;
|
|
653
|
+
stdout.write(`Command ${command.id}: ${command.command_type} (${suffix})\n`);
|
|
654
|
+
}
|
|
655
|
+
function writeSpoolQueued(stdout, options, type, file) {
|
|
656
|
+
if (options.json) {
|
|
657
|
+
stdout.write(`${JSON.stringify({ type: 'bridge_spool_queued', record_type: type, file })}\n`);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
stdout.write(`Queued ${type} in local bridge spool: ${file}\n`);
|
|
661
|
+
}
|
|
662
|
+
function writeSpoolFlushStatus(stdout, options, result) {
|
|
663
|
+
if (result.flushed === 0 && result.remaining === 0 && result.failed === 0)
|
|
664
|
+
return;
|
|
665
|
+
if (options.json) {
|
|
666
|
+
stdout.write(`${JSON.stringify({ type: 'bridge_spool_flush', ...result })}\n`);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
stdout.write(`Bridge spool: ${result.flushed} flushed, ${result.remaining} remaining.\n`);
|
|
670
|
+
}
|
|
671
|
+
function writeArtifactUploadStatus(stdout, options, artifact) {
|
|
672
|
+
if (options.json) {
|
|
673
|
+
stdout.write(`${JSON.stringify({ success: true, data: { artifact } })}\n`);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
stdout.write(`Uploaded artifact ${artifact.id}: ${artifact.name ?? 'artifact'}\n`);
|
|
677
|
+
}
|
|
678
|
+
function writeBridgeHealthStatus(stdout, options, health) {
|
|
679
|
+
if (options.json) {
|
|
680
|
+
stdout.write(`${JSON.stringify({ success: true, data: { bridge_health: health } })}\n`);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const summary = health.summary ?? {};
|
|
684
|
+
const line = [
|
|
685
|
+
`Bridge health: ${numberValue(summary.online)} online`,
|
|
686
|
+
`${numberValue(summary.needs_attention)} attention`,
|
|
687
|
+
`${numberValue(summary.queued_commands)} queued`,
|
|
688
|
+
`${numberValue(summary.delivered_commands)} in flight`,
|
|
689
|
+
`${numberValue(summary.unsafe_commands)} unsafe`,
|
|
690
|
+
];
|
|
691
|
+
stdout.write(`${line.join(', ')}.\n`);
|
|
692
|
+
for (const item of Array.isArray(health.clients) ? health.clients.slice(0, 10) : []) {
|
|
693
|
+
const client = objectValue(item.client);
|
|
694
|
+
const freshness = objectValue(item.freshness);
|
|
695
|
+
const name = stringValue(client.name) ?? stringValue(client.id) ?? 'unknown-client';
|
|
696
|
+
const status = stringValue(freshness.status) ?? 'unknown';
|
|
697
|
+
const age = freshness.age_seconds === null ? 'never' : `${numberValue(freshness.age_seconds)}s ago`;
|
|
698
|
+
const runs = numberValue(item.active_run_count);
|
|
699
|
+
const commands = numberValue(item.queued_command_count) + numberValue(item.delivered_command_count);
|
|
700
|
+
stdout.write(`- ${name}: ${status}, seen ${age}, runs ${runs}, commands ${commands}\n`);
|
|
701
|
+
const reasons = Array.isArray(item.attention_reasons) ? item.attention_reasons.filter((reason) => typeof reason === 'string') : [];
|
|
702
|
+
if (reasons.length > 0)
|
|
703
|
+
stdout.write(` attention: ${reasons.map((reason) => reason.replace(/_/g, ' ')).join(', ')}\n`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function writeBridgeTokenCreateStatus(stdout, options, data) {
|
|
707
|
+
const commands = bridgeTokenFollowUpCommands(data.token, data.client, options);
|
|
708
|
+
if (options.json) {
|
|
709
|
+
stdout.write(`${JSON.stringify({ success: true, data: { ...data, commands } })}\n`);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const clientName = stringValue(data.client.name) ?? options.clientName;
|
|
713
|
+
const clientId = stringValue(data.client.id) ?? 'unknown-client';
|
|
714
|
+
stdout.write(`Created bridge-client token ${data.token.key_prefix}... for ${clientName} (${clientId}).\n`);
|
|
715
|
+
stdout.write('This token is shown once. Store it securely and use it only for this local wrapper client.\n');
|
|
716
|
+
stdout.write(`Token: ${data.token.api_key}\n`);
|
|
717
|
+
stdout.write(`Expires: ${data.token.expires_at}\n`);
|
|
718
|
+
stdout.write('Next:\n');
|
|
719
|
+
stdout.write(` ${commands.doctor}\n`);
|
|
720
|
+
stdout.write(` ${commands.run}\n`);
|
|
721
|
+
}
|
|
722
|
+
function writeEventBatchStatus(stdout, options, result) {
|
|
723
|
+
if (options.json) {
|
|
724
|
+
stdout.write(`${JSON.stringify({ success: true, data: result })}\n`);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
stdout.write(`Bridge events accepted: ${result.accepted}, deduped: ${result.deduped}.\n`);
|
|
728
|
+
}
|
|
729
|
+
function writeBridgeWrapperDoctorStatus(stdout, options, report) {
|
|
730
|
+
if (options.json) {
|
|
731
|
+
stdout.write(`${JSON.stringify({ success: report.ready, data: report })}\n`);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
stdout.write(`Bridge wrapper doctor: ${report.ready ? 'ready' : 'needs attention'}.\n`);
|
|
735
|
+
for (const check of report.checks) {
|
|
736
|
+
stdout.write(`- ${check.status}: ${check.message}\n`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function writeFailure(options, code, message, details) {
|
|
740
|
+
if (options.json) {
|
|
741
|
+
const writer = options.stdout ?? process.stdout;
|
|
742
|
+
writer.write(`${JSON.stringify({ success: false, error: { code, message, details } })}\n`);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
const writer = options.stderr ?? process.stderr;
|
|
746
|
+
writer.write(`Error: ${message}\nCode: ${code}\n`);
|
|
747
|
+
}
|
|
748
|
+
function bridgeTokenFollowUpCommands(token, client, options) {
|
|
749
|
+
const clientName = stringValue(client.name) ?? options.clientName;
|
|
750
|
+
const clientType = stringValue(client.type) ?? options.clientType;
|
|
751
|
+
return {
|
|
752
|
+
doctor: `HYPERTAB_API_KEY=${shellQuote(token.api_key)} npx @hypertabai/mcp bridge wrapper doctor --path .hypertab/bridge-wrapper.mjs --json`,
|
|
753
|
+
run: [
|
|
754
|
+
`HYPERTAB_API_KEY=${shellQuote(token.api_key)}`,
|
|
755
|
+
`HYPERTAB_BRIDGE_CLIENT_NAME=${shellQuote(clientName)}`,
|
|
756
|
+
`HYPERTAB_BRIDGE_CLIENT_TYPE=${shellQuote(clientType)}`,
|
|
757
|
+
'node .hypertab/bridge-wrapper.mjs "Report local bridge health"',
|
|
758
|
+
].join(' '),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function objectValue(value) {
|
|
762
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
763
|
+
}
|
|
764
|
+
function stringValue(value) {
|
|
765
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
766
|
+
}
|
|
767
|
+
function numberValue(value) {
|
|
768
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
769
|
+
}
|
|
770
|
+
function optionalTrimmedString(value) {
|
|
771
|
+
const trimmed = value?.trim();
|
|
772
|
+
return trimmed ? trimmed : undefined;
|
|
773
|
+
}
|
|
774
|
+
function shellQuote(value) {
|
|
775
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
776
|
+
}
|
|
777
|
+
function formatByteSize(bytes) {
|
|
778
|
+
if (bytes < 1024)
|
|
779
|
+
return `${bytes} B`;
|
|
780
|
+
if (bytes < 1024 * 1024)
|
|
781
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
782
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
783
|
+
}
|
|
784
|
+
async function readBridgeEventBatchFile(file) {
|
|
785
|
+
const raw = JSON.parse(await readFile(file, 'utf8'));
|
|
786
|
+
const events = Array.isArray(raw) ? raw : objectValue(raw).events;
|
|
787
|
+
if (!Array.isArray(events))
|
|
788
|
+
throw new Error('Bridge event batch file must contain an events array.');
|
|
789
|
+
if (events.length === 0 || events.length > 250)
|
|
790
|
+
throw new Error('Bridge event batch must contain 1 to 250 events.');
|
|
791
|
+
return {
|
|
792
|
+
events: events.map((event, index) => normalizeBridgeEvent(event, index)),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function normalizeBridgeEvent(event, index) {
|
|
796
|
+
const input = objectValue(event);
|
|
797
|
+
const runId = stringValue(input.run_id);
|
|
798
|
+
const type = stringValue(input.type);
|
|
799
|
+
const message = stringValue(input.message);
|
|
800
|
+
if (!runId)
|
|
801
|
+
throw new Error(`Event ${index + 1} is missing run_id.`);
|
|
802
|
+
if (!type || !isBridgeEventType(type))
|
|
803
|
+
throw new Error(`Event ${index + 1} has an invalid type.`);
|
|
804
|
+
if (!message)
|
|
805
|
+
throw new Error(`Event ${index + 1} is missing message.`);
|
|
806
|
+
const data = objectValue(input.data);
|
|
807
|
+
const idempotencyKey = stringValue(input.idempotency_key);
|
|
808
|
+
return {
|
|
809
|
+
run_id: runId,
|
|
810
|
+
type,
|
|
811
|
+
message,
|
|
812
|
+
data: Object.keys(data).length > 0 ? data : undefined,
|
|
813
|
+
idempotency_key: idempotencyKey ?? undefined,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function isBridgeEventType(value) {
|
|
817
|
+
return ['message', 'step', 'tool_call', 'file_change', 'artifact', 'approval', 'error', 'status'].includes(value);
|
|
818
|
+
}
|
|
819
|
+
function normalizeBridgeClientType(value) {
|
|
820
|
+
if (BRIDGE_CLIENT_TYPES.includes(value))
|
|
821
|
+
return value;
|
|
822
|
+
throw new Error(`Invalid bridge client type '${value}'. Use one of: ${BRIDGE_CLIENT_TYPES.join(', ')}.`);
|
|
823
|
+
}
|
|
824
|
+
function normalizeCommandResultStatus(value) {
|
|
825
|
+
if (value === 'completed' || value === 'failed' || value === 'cancelled')
|
|
826
|
+
return value;
|
|
827
|
+
throw new Error(`Invalid command result status '${value}'. Use completed, failed, or cancelled.`);
|
|
828
|
+
}
|
|
829
|
+
function normalizeWrapperType(value) {
|
|
830
|
+
if (value === 'node' || value === 'shell')
|
|
831
|
+
return value;
|
|
832
|
+
throw new Error(`Invalid wrapper type '${value}'. Use node or shell.`);
|
|
833
|
+
}
|
|
834
|
+
function inferWrapperType(path) {
|
|
835
|
+
if (path.endsWith('.sh'))
|
|
836
|
+
return 'shell';
|
|
837
|
+
if (path.endsWith('.mjs') || path.endsWith('.js'))
|
|
838
|
+
return 'node';
|
|
839
|
+
return 'unknown';
|
|
840
|
+
}
|
|
841
|
+
function wrapperTemplateName(type) {
|
|
842
|
+
return type === 'shell' ? 'bridge-wrapper.sh' : 'bridge-wrapper.mjs';
|
|
843
|
+
}
|
|
844
|
+
function addStarterMarkerCheck(checks, body, id, marker) {
|
|
845
|
+
checks.push(body.includes(marker)
|
|
846
|
+
? { id, status: 'pass', message: `Wrapper references ${marker}.` }
|
|
847
|
+
: {
|
|
848
|
+
id,
|
|
849
|
+
status: 'warn',
|
|
850
|
+
message: `Wrapper does not reference ${marker}; custom wrappers should provide equivalent behavior.`,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
async function fileExists(path) {
|
|
854
|
+
try {
|
|
855
|
+
await access(path);
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function parsePositiveInt(value) {
|
|
863
|
+
const parsed = Number.parseInt(value, 10);
|
|
864
|
+
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
865
|
+
throw new Error(`Expected a positive integer, received '${value}'.`);
|
|
866
|
+
return parsed;
|
|
867
|
+
}
|
|
868
|
+
function defaultRunTitle(objective) {
|
|
869
|
+
const trimmed = objective.trim();
|
|
870
|
+
return trimmed.length > 120 ? `${trimmed.slice(0, 117)}...` : trimmed;
|
|
871
|
+
}
|
|
872
|
+
function sleep(ms) {
|
|
873
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
874
|
+
}
|
|
875
|
+
//# sourceMappingURL=index.js.map
|