@ramxvnn/bridge 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/doctor.d.ts +35 -2
- package/dist/src/commands/doctor.js +290 -55
- package/dist/src/lib/ramx.d.ts +2 -2
- package/dist/src/lib/ramx.js +1 -1
- package/package.json +2 -2
|
@@ -5,15 +5,48 @@
|
|
|
5
5
|
* non-technical user who sees "403" learns nothing; "this key cannot post,
|
|
6
6
|
* reconnect and allow posting" they can act on.
|
|
7
7
|
*
|
|
8
|
+
* There are two shapes of setup on disk, and doctor has to understand both:
|
|
9
|
+
*
|
|
10
|
+
* - the LEGACY single-agent setup in `config.json`, which assumed one
|
|
11
|
+
* framework install meant one RAM/X agent;
|
|
12
|
+
* - the per-binding credentials in `bindings.json`, one RAM/X agent per
|
|
13
|
+
* OpenClaw agent or Hermes profile.
|
|
14
|
+
*
|
|
15
|
+
* It used to read only the first. Anyone who connected through
|
|
16
|
+
* `openclaw ramx connect` or `ramx-bridge hermes` — the flows this product
|
|
17
|
+
* actually leads with — was told "No setup found on this computer", and then
|
|
18
|
+
* told to run `init`, a single-agent command that is the wrong fix for them.
|
|
19
|
+
* Their credentials were sitting in `bindings.json` the whole time.
|
|
20
|
+
*
|
|
21
|
+
* Each binding is checked with ITS OWN credential and nothing else. That is
|
|
22
|
+
* the same fail-closed rule `resolveMcpCredential` follows, and for the same
|
|
23
|
+
* reason: a binding that borrowed another's key would report healthy while
|
|
24
|
+
* the agent it names is broken. One failing binding must not stop the others
|
|
25
|
+
* being checked either — a user with four bots wants all four verdicts.
|
|
26
|
+
*
|
|
8
27
|
* No secret is printed, not even partially.
|
|
9
28
|
*/
|
|
10
29
|
import { Ramx } from '../lib/ramx.js';
|
|
30
|
+
import { type RamxBinding } from '../lib/bindings.js';
|
|
11
31
|
export interface CheckResult {
|
|
12
32
|
name: string;
|
|
13
33
|
status: 'ok' | 'fail' | 'warn';
|
|
14
34
|
detail: string;
|
|
15
35
|
fix?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Which setup this check belongs to — "OpenClaw", "Hermes", "Legacy setup".
|
|
38
|
+
* Absent for checks about the machine as a whole.
|
|
39
|
+
*/
|
|
40
|
+
group?: string;
|
|
16
41
|
}
|
|
17
|
-
/**
|
|
18
|
-
export
|
|
42
|
+
/** Test seam: each binding gets its own client, never a shared one. */
|
|
43
|
+
export type ClientFactory = (binding: RamxBinding) => Ramx;
|
|
44
|
+
/**
|
|
45
|
+
* Pure so the tests can assert the diagnosis without a terminal.
|
|
46
|
+
*
|
|
47
|
+
* `client` stands in for the network on the LEGACY config path only.
|
|
48
|
+
* `makeClient` does the same per binding. A binding never borrows `client`;
|
|
49
|
+
* see the module note.
|
|
50
|
+
*/
|
|
51
|
+
export declare function runChecks(client?: Ramx, makeClient?: ClientFactory): Promise<CheckResult[]>;
|
|
19
52
|
export declare function runDoctor(): Promise<number>;
|
|
@@ -5,17 +5,242 @@
|
|
|
5
5
|
* non-technical user who sees "403" learns nothing; "this key cannot post,
|
|
6
6
|
* reconnect and allow posting" they can act on.
|
|
7
7
|
*
|
|
8
|
+
* There are two shapes of setup on disk, and doctor has to understand both:
|
|
9
|
+
*
|
|
10
|
+
* - the LEGACY single-agent setup in `config.json`, which assumed one
|
|
11
|
+
* framework install meant one RAM/X agent;
|
|
12
|
+
* - the per-binding credentials in `bindings.json`, one RAM/X agent per
|
|
13
|
+
* OpenClaw agent or Hermes profile.
|
|
14
|
+
*
|
|
15
|
+
* It used to read only the first. Anyone who connected through
|
|
16
|
+
* `openclaw ramx connect` or `ramx-bridge hermes` — the flows this product
|
|
17
|
+
* actually leads with — was told "No setup found on this computer", and then
|
|
18
|
+
* told to run `init`, a single-agent command that is the wrong fix for them.
|
|
19
|
+
* Their credentials were sitting in `bindings.json` the whole time.
|
|
20
|
+
*
|
|
21
|
+
* Each binding is checked with ITS OWN credential and nothing else. That is
|
|
22
|
+
* the same fail-closed rule `resolveMcpCredential` follows, and for the same
|
|
23
|
+
* reason: a binding that borrowed another's key would report healthy while
|
|
24
|
+
* the agent it names is broken. One failing binding must not stop the others
|
|
25
|
+
* being checked either — a user with four bots wants all four verdicts.
|
|
26
|
+
*
|
|
8
27
|
* No secret is printed, not even partially.
|
|
9
28
|
*/
|
|
10
29
|
import { Ramx } from '../lib/ramx.js';
|
|
11
30
|
import { readConfig, configPath, SOURCES, runtimeTransport } from '../lib/config.js';
|
|
31
|
+
import { listBindings, bindingsPath } from '../lib/bindings.js';
|
|
12
32
|
import { say, ok, fail, warn, bold, dim, cyan, redact } from '../lib/ui.js';
|
|
13
|
-
import { trialSummary } from '../lib/trial.js';
|
|
14
|
-
|
|
15
|
-
|
|
33
|
+
import { trialSummary, isClaimRequiredError } from '../lib/trial.js';
|
|
34
|
+
const FRAMEWORK_LABEL = {
|
|
35
|
+
openclaw: 'OpenClaw',
|
|
36
|
+
hermes: 'Hermes',
|
|
37
|
+
mcp: 'MCP client',
|
|
38
|
+
bot: 'Bot / custom runtime',
|
|
39
|
+
};
|
|
40
|
+
/** What to call this binding on screen. Never the RAM/X handle alone. */
|
|
41
|
+
function bindingLabel(b) {
|
|
42
|
+
const local = b.localDisplayName?.trim() || b.localAgentId;
|
|
43
|
+
return b.framework === 'hermes' ? `${local} (profile)` : local;
|
|
44
|
+
}
|
|
45
|
+
/** The command that reconnects THIS binding, and only this one. */
|
|
46
|
+
function reconnectFix(b) {
|
|
47
|
+
if (b.framework === 'openclaw') {
|
|
48
|
+
return `Reconnect just this agent: openclaw ramx connect --agent ${b.localAgentId}`;
|
|
49
|
+
}
|
|
50
|
+
if (b.framework === 'hermes') {
|
|
51
|
+
return `Reconnect just this profile: npx @ramxvnn/bridge hermes --profile ${b.localAgentId}`;
|
|
52
|
+
}
|
|
53
|
+
return 'Reconnect this runtime: npx @ramxvnn/bridge init';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Turns a failed call into something the reader can act on.
|
|
57
|
+
*
|
|
58
|
+
* The distinctions matter: 401 means the credential is gone, 403 means it is
|
|
59
|
+
* valid but not allowed, 429 means nothing is wrong at all, and a network
|
|
60
|
+
* error means we learned nothing about the credential either way. Collapsing
|
|
61
|
+
* them into "auth failed" sends people to reconnect a binding that is fine.
|
|
62
|
+
*/
|
|
63
|
+
function diagnose(err, subject, fixWhenRejected) {
|
|
64
|
+
const e = err;
|
|
65
|
+
if (isClaimRequiredError(err)) {
|
|
66
|
+
return {
|
|
67
|
+
name: subject,
|
|
68
|
+
status: 'fail',
|
|
69
|
+
detail: 'The 7-day trial has ended and nobody has claimed this agent.',
|
|
70
|
+
fix: 'Claim it to start posting again: https://ramx.vn/claim',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (e?.isAuth) {
|
|
74
|
+
return {
|
|
75
|
+
name: subject,
|
|
76
|
+
status: 'fail',
|
|
77
|
+
detail: 'RAM/X no longer accepts this connection — the key was revoked or removed.',
|
|
78
|
+
fix: fixWhenRejected,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (e?.isScope) {
|
|
82
|
+
return {
|
|
83
|
+
name: subject,
|
|
84
|
+
status: 'fail',
|
|
85
|
+
detail: 'This connection is not allowed to do that.',
|
|
86
|
+
fix: fixWhenRejected,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (e?.isRateLimited) {
|
|
90
|
+
return {
|
|
91
|
+
name: subject,
|
|
92
|
+
status: 'warn',
|
|
93
|
+
detail: 'RAM/X is rate-limiting this computer right now. Nothing is wrong with the connection.',
|
|
94
|
+
fix: e.retryAfterSeconds
|
|
95
|
+
? `Wait about ${e.retryAfterSeconds}s and run this again.`
|
|
96
|
+
: 'Wait a minute and run this again.',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
name: subject,
|
|
101
|
+
status: 'fail',
|
|
102
|
+
detail: `Could not reach RAM/X. ${redact(err)}`,
|
|
103
|
+
fix: 'Check your internet connection, then try again.',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The claim link, with its one-time token removed.
|
|
108
|
+
*
|
|
109
|
+
* `claimUrl` carries `?token=ramx_claim_…`, and that token TRANSFERS
|
|
110
|
+
* OWNERSHIP of the agent to whoever opens it. Every other surface that
|
|
111
|
+
* prints it — the connect flow, `status` — is showing it once to the person
|
|
112
|
+
* who just connected. Doctor is different: it is the output people paste
|
|
113
|
+
* into bug reports and support threads, which is exactly why the server
|
|
114
|
+
* refuses to put this token in API error bodies. So doctor shows the landing
|
|
115
|
+
* page and says where the full link lives.
|
|
116
|
+
*/
|
|
117
|
+
function claimLinkForDisplay(claimUrl) {
|
|
118
|
+
if (!claimUrl)
|
|
119
|
+
return 'https://ramx.vn/claim';
|
|
120
|
+
try {
|
|
121
|
+
const u = new URL(claimUrl);
|
|
122
|
+
return `${u.origin}${u.pathname}`;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return 'https://ramx.vn/claim';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** The trial/ownership line for one connection, or nothing for an owned agent. */
|
|
129
|
+
function ownershipCheck(me, claimUrl, group) {
|
|
130
|
+
if (me.trial?.provisional) {
|
|
131
|
+
const link = claimLinkForDisplay(claimUrl);
|
|
132
|
+
const where = claimUrl
|
|
133
|
+
? ' Your one-time claim link was shown when you connected.'
|
|
134
|
+
: '';
|
|
135
|
+
return me.trial.expired
|
|
136
|
+
? {
|
|
137
|
+
name: 'Ownership',
|
|
138
|
+
status: 'fail',
|
|
139
|
+
detail: 'The 7-day trial has ended and nobody has claimed this agent.',
|
|
140
|
+
fix: `Claim it to start posting again: ${link}.${where}`,
|
|
141
|
+
...(group ? { group } : {}),
|
|
142
|
+
}
|
|
143
|
+
: {
|
|
144
|
+
name: 'Ownership',
|
|
145
|
+
status: 'warn',
|
|
146
|
+
detail: `Unclaimed — trial, ${trialSummary(me.trial)}.`,
|
|
147
|
+
fix: `Claim it any time to keep it: ${link}.${where}`,
|
|
148
|
+
...(group ? { group } : {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (me.ownerBound) {
|
|
152
|
+
return { name: 'Ownership', status: 'ok', detail: 'Claimed', ...(group ? { group } : {}) };
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Checks one binding with its own credential.
|
|
158
|
+
*
|
|
159
|
+
* Never falls back to another binding's key, and never to `config.json`: a
|
|
160
|
+
* named binding that cannot authenticate must fail, not quietly succeed as
|
|
161
|
+
* somebody else.
|
|
162
|
+
*/
|
|
163
|
+
async function checkBinding(b, makeClient) {
|
|
164
|
+
const group = FRAMEWORK_LABEL[b.framework] ?? b.framework;
|
|
165
|
+
const label = bindingLabel(b);
|
|
166
|
+
const out = [];
|
|
167
|
+
if (!b.apiKey) {
|
|
168
|
+
out.push({
|
|
169
|
+
name: label,
|
|
170
|
+
status: 'fail',
|
|
171
|
+
detail: 'This connection has no RAM/X key saved.',
|
|
172
|
+
fix: reconnectFix(b),
|
|
173
|
+
group,
|
|
174
|
+
});
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
const client = makeClient
|
|
178
|
+
? makeClient(b)
|
|
179
|
+
: new Ramx({ apiKey: b.apiKey, apiBase: b.apiBase });
|
|
180
|
+
let me;
|
|
181
|
+
try {
|
|
182
|
+
me = await client.getMe();
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const d = diagnose(err, label, reconnectFix(b));
|
|
186
|
+
out.push({ ...d, group });
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
out.push({ name: label, status: 'ok', detail: `Connected as ${me.agent.handle}`, group });
|
|
190
|
+
if (me.agent.status === 'deleted' || me.agent.status === 'suspended') {
|
|
191
|
+
out.push({
|
|
192
|
+
name: `${label} — agent status`,
|
|
193
|
+
status: 'fail',
|
|
194
|
+
detail: `The RAM/X agent behind this connection is ${me.agent.status}.`,
|
|
195
|
+
fix: reconnectFix(b),
|
|
196
|
+
group,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const scopes = me.apiKey.scopes ?? [];
|
|
200
|
+
if (!scopes.includes('read')) {
|
|
201
|
+
out.push({
|
|
202
|
+
name: `${label} — reading`,
|
|
203
|
+
status: 'fail',
|
|
204
|
+
detail: 'This connection cannot read the feed.',
|
|
205
|
+
fix: reconnectFix(b),
|
|
206
|
+
group,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else if (!scopes.includes('post')) {
|
|
210
|
+
out.push({
|
|
211
|
+
name: `${label} — posting`,
|
|
212
|
+
status: 'warn',
|
|
213
|
+
detail: 'This connection can read but not post.',
|
|
214
|
+
fix: `If this one should publish, ${reconnectFix(b).replace(/^Reconnect/, 'reconnect')}`,
|
|
215
|
+
group,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
const ownership = ownershipCheck(me, b.claimUrl, group);
|
|
219
|
+
if (ownership)
|
|
220
|
+
out.push({ ...ownership, name: `${label} — ownership` });
|
|
221
|
+
if (me.credential?.rotationAvailable) {
|
|
222
|
+
out.push({
|
|
223
|
+
name: `${label} — credential`,
|
|
224
|
+
status: 'warn',
|
|
225
|
+
detail: 'This agent has been claimed, but this computer still holds its trial key.',
|
|
226
|
+
fix: 'Upgrade it in place — no reconnect needed: npx @ramxvnn/bridge pair --refresh',
|
|
227
|
+
group,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Pure so the tests can assert the diagnosis without a terminal.
|
|
234
|
+
*
|
|
235
|
+
* `client` stands in for the network on the LEGACY config path only.
|
|
236
|
+
* `makeClient` does the same per binding. A binding never borrows `client`;
|
|
237
|
+
* see the module note.
|
|
238
|
+
*/
|
|
239
|
+
export async function runChecks(client, makeClient) {
|
|
16
240
|
const results = [];
|
|
17
241
|
const config = readConfig();
|
|
18
|
-
|
|
242
|
+
const bindings = listBindings();
|
|
243
|
+
if (!config && bindings.length === 0) {
|
|
19
244
|
results.push({
|
|
20
245
|
name: 'Setup',
|
|
21
246
|
status: 'fail',
|
|
@@ -24,13 +249,45 @@ export async function runChecks(client) {
|
|
|
24
249
|
});
|
|
25
250
|
return results;
|
|
26
251
|
}
|
|
27
|
-
|
|
252
|
+
// Bindings first: this is the shape most people have, and a user with
|
|
253
|
+
// four connected bots should see their four verdicts before anything about
|
|
254
|
+
// a legacy file they may not have.
|
|
255
|
+
if (bindings.length > 0) {
|
|
256
|
+
results.push({
|
|
257
|
+
name: 'Connected agents',
|
|
258
|
+
status: 'ok',
|
|
259
|
+
detail: `${bindings.length} found in ${bindingsPath()}`,
|
|
260
|
+
});
|
|
261
|
+
// Grouped by framework here rather than relying on `listBindings`, which
|
|
262
|
+
// sorts by local id alone: `openclaw:alpha, hermes:beta, openclaw:gamma`
|
|
263
|
+
// would interleave and print the "OpenClaw" header twice.
|
|
264
|
+
const ordered = [...bindings].sort((x, y) => (FRAMEWORK_LABEL[x.framework] ?? x.framework).localeCompare(FRAMEWORK_LABEL[y.framework] ?? y.framework) || bindingLabel(x).localeCompare(bindingLabel(y)));
|
|
265
|
+
// Sequential on purpose: these are rate-limited calls against one host,
|
|
266
|
+
// and a stampede from a user with a dozen bindings would produce 429s
|
|
267
|
+
// that look like failures.
|
|
268
|
+
for (const b of ordered) {
|
|
269
|
+
// One binding must never stop the rest being checked.
|
|
270
|
+
results.push(...(await checkBinding(b, makeClient)));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (config)
|
|
274
|
+
results.push(...(await checkLegacyConfig(config, client)));
|
|
275
|
+
return results;
|
|
276
|
+
}
|
|
277
|
+
/** The original single-agent checks, unchanged in substance. */
|
|
278
|
+
async function checkLegacyConfig(config, client) {
|
|
279
|
+
const results = [];
|
|
280
|
+
// Only labelled as "legacy" when there is something to distinguish it from.
|
|
281
|
+
const group = listBindings().length > 0 ? 'Legacy setup' : undefined;
|
|
282
|
+
const g = group ? { group } : {};
|
|
283
|
+
results.push({ name: 'Setup', status: 'ok', detail: `Found at ${configPath()}`, ...g });
|
|
28
284
|
if (!config.ramx?.apiKey) {
|
|
29
285
|
results.push({
|
|
30
286
|
name: 'RAM/X key',
|
|
31
287
|
status: 'fail',
|
|
32
288
|
detail: 'The saved setup has no RAM/X key.',
|
|
33
289
|
fix: 'Run setup again: npx @ramxvnn/bridge init',
|
|
290
|
+
...g,
|
|
34
291
|
});
|
|
35
292
|
return results;
|
|
36
293
|
}
|
|
@@ -38,75 +295,42 @@ export async function runChecks(client) {
|
|
|
38
295
|
let me;
|
|
39
296
|
try {
|
|
40
297
|
me = await ramx.getMe();
|
|
41
|
-
results.push({ name: 'RAM/X account', status: 'ok', detail: `Connected as ${me.agent.handle}
|
|
298
|
+
results.push({ name: 'RAM/X account', status: 'ok', detail: `Connected as ${me.agent.handle}`, ...g });
|
|
42
299
|
}
|
|
43
300
|
catch (err) {
|
|
44
|
-
const
|
|
45
|
-
results.push({
|
|
46
|
-
name: 'RAM/X account',
|
|
47
|
-
status: 'fail',
|
|
48
|
-
detail: e.isAuth
|
|
49
|
-
? 'RAM/X no longer accepts this connection.'
|
|
50
|
-
: `Could not reach RAM/X. ${redact(err)}`,
|
|
51
|
-
fix: e.isAuth
|
|
52
|
-
? 'The connection may have been removed in your dashboard. Run: npx @ramxvnn/bridge init'
|
|
53
|
-
: 'Check your internet connection, then try again.',
|
|
54
|
-
});
|
|
301
|
+
const d = diagnose(err, 'RAM/X account', 'The connection may have been removed in your dashboard. Run: npx @ramxvnn/bridge init');
|
|
302
|
+
results.push({ ...d, ...g });
|
|
55
303
|
return results;
|
|
56
304
|
}
|
|
57
|
-
// Scopes, expressed as capabilities rather than scope names.
|
|
58
305
|
const scopes = me.apiKey.scopes ?? [];
|
|
59
306
|
results.push(scopes.includes('read')
|
|
60
|
-
? { name: 'Can read RAM/X', status: 'ok', detail: 'Yes' }
|
|
307
|
+
? { name: 'Can read RAM/X', status: 'ok', detail: 'Yes', ...g }
|
|
61
308
|
: {
|
|
62
309
|
name: 'Can read RAM/X',
|
|
63
310
|
status: 'fail',
|
|
64
311
|
detail: 'This connection cannot read the feed.',
|
|
65
312
|
fix: 'Run setup again and approve reading: npx @ramxvnn/bridge init',
|
|
313
|
+
...g,
|
|
66
314
|
});
|
|
67
315
|
results.push(scopes.includes('post')
|
|
68
|
-
? { name: 'Can post to RAM/X', status: 'ok', detail: 'Yes' }
|
|
316
|
+
? { name: 'Can post to RAM/X', status: 'ok', detail: 'Yes', ...g }
|
|
69
317
|
: {
|
|
70
318
|
name: 'Can post to RAM/X',
|
|
71
319
|
status: 'warn',
|
|
72
320
|
detail: 'This connection can read but not post.',
|
|
73
321
|
fix: 'If your bot should publish, run setup again: npx @ramxvnn/bridge init',
|
|
322
|
+
...g,
|
|
74
323
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (me.trial?.provisional) {
|
|
79
|
-
results.push(me.trial.expired
|
|
80
|
-
? {
|
|
81
|
-
name: 'Ownership',
|
|
82
|
-
status: 'fail',
|
|
83
|
-
detail: 'The 7-day trial has ended and nobody has claimed this agent.',
|
|
84
|
-
fix: config.ramx.claimUrl
|
|
85
|
-
? `Claim it to start posting again: ${config.ramx.claimUrl}`
|
|
86
|
-
: 'Claim it at https://ramx.vn/claim to start posting again.',
|
|
87
|
-
}
|
|
88
|
-
: {
|
|
89
|
-
name: 'Ownership',
|
|
90
|
-
status: 'warn',
|
|
91
|
-
detail: `Unclaimed — trial, ${trialSummary(me.trial)}.`,
|
|
92
|
-
fix: config.ramx.claimUrl
|
|
93
|
-
? `Claim it any time to keep it: ${config.ramx.claimUrl}`
|
|
94
|
-
: 'Claim it any time at https://ramx.vn/claim.',
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
else if (me.ownerBound) {
|
|
98
|
-
results.push({ name: 'Ownership', status: 'ok', detail: 'Claimed' });
|
|
99
|
-
}
|
|
100
|
-
// Someone claimed the agent while this runtime kept running. Nothing is
|
|
101
|
-
// broken — but the local credential is still the trial one, and swapping
|
|
102
|
-
// it is a single authenticated call, so say so instead of leaving the
|
|
103
|
-
// user to wonder whether they need to reconnect. They do not.
|
|
324
|
+
const ownership = ownershipCheck(me, config.ramx.claimUrl, group);
|
|
325
|
+
if (ownership)
|
|
326
|
+
results.push(ownership);
|
|
104
327
|
if (me.credential?.rotationAvailable) {
|
|
105
328
|
results.push({
|
|
106
329
|
name: 'Credential',
|
|
107
330
|
status: 'warn',
|
|
108
331
|
detail: 'This agent has been claimed, but this computer still holds its trial key.',
|
|
109
332
|
fix: 'Upgrade it in place — no reconnect needed: npx @ramxvnn/bridge pair --refresh',
|
|
333
|
+
...g,
|
|
110
334
|
});
|
|
111
335
|
}
|
|
112
336
|
if (me.agent.status === 'deleted' || me.agent.status === 'suspended') {
|
|
@@ -115,22 +339,24 @@ export async function runChecks(client) {
|
|
|
115
339
|
status: 'fail',
|
|
116
340
|
detail: `This agent is ${me.agent.status}.`,
|
|
117
341
|
fix: 'Connect a different agent: npx @ramxvnn/bridge init',
|
|
342
|
+
...g,
|
|
118
343
|
});
|
|
119
344
|
}
|
|
120
345
|
else {
|
|
121
|
-
results.push({ name: 'Agent status', status: 'ok', detail: me.agent.status });
|
|
346
|
+
results.push({ name: 'Agent status', status: 'ok', detail: me.agent.status, ...g });
|
|
122
347
|
}
|
|
123
348
|
// Platform side.
|
|
124
349
|
const spec = SOURCES[config.source];
|
|
125
350
|
if (spec && spec.fields.length > 0) {
|
|
126
351
|
const missing = spec.fields.filter((f) => !config.platform?.[f.key]);
|
|
127
352
|
results.push(missing.length === 0
|
|
128
|
-
? { name: `${spec.label} details`, status: 'ok', detail: 'Saved on this computer' }
|
|
353
|
+
? { name: `${spec.label} details`, status: 'ok', detail: 'Saved on this computer', ...g }
|
|
129
354
|
: {
|
|
130
355
|
name: `${spec.label} details`,
|
|
131
356
|
status: 'fail',
|
|
132
357
|
detail: `${missing.length} value(s) missing.`,
|
|
133
358
|
fix: 'Run setup again: npx @ramxvnn/bridge init',
|
|
359
|
+
...g,
|
|
134
360
|
});
|
|
135
361
|
}
|
|
136
362
|
if (spec?.needsPublicUrl) {
|
|
@@ -139,6 +365,7 @@ export async function runChecks(client) {
|
|
|
139
365
|
status: 'warn',
|
|
140
366
|
detail: 'This source needs a public web address pointing at this computer.',
|
|
141
367
|
fix: 'Set up a tunnel or a public URL, then restart the bridge.',
|
|
368
|
+
...g,
|
|
142
369
|
});
|
|
143
370
|
}
|
|
144
371
|
if (spec && !runtimeTransport(config.source) && config.source !== 'mcp' && config.source !== 'api_web_custom') {
|
|
@@ -146,6 +373,7 @@ export async function runChecks(client) {
|
|
|
146
373
|
name: 'Bot connection',
|
|
147
374
|
status: 'warn',
|
|
148
375
|
detail: 'There is no built-in adapter for this source.',
|
|
376
|
+
...g,
|
|
149
377
|
});
|
|
150
378
|
}
|
|
151
379
|
return results;
|
|
@@ -153,15 +381,22 @@ export async function runChecks(client) {
|
|
|
153
381
|
export async function runDoctor() {
|
|
154
382
|
say(bold('\nRAM/X Bridge — checkup\n'));
|
|
155
383
|
const results = await runChecks();
|
|
384
|
+
let lastGroup;
|
|
156
385
|
for (const r of results) {
|
|
386
|
+
if (r.group !== lastGroup) {
|
|
387
|
+
if (r.group)
|
|
388
|
+
say(bold(`\n${r.group}`));
|
|
389
|
+
lastGroup = r.group;
|
|
390
|
+
}
|
|
391
|
+
const indent = r.group ? ' ' : '';
|
|
157
392
|
if (r.status === 'ok')
|
|
158
|
-
ok(`${r.name}: ${r.detail}`);
|
|
393
|
+
ok(`${indent}${r.name}: ${r.detail}`);
|
|
159
394
|
else if (r.status === 'warn')
|
|
160
|
-
warn(`${r.name}: ${r.detail}`);
|
|
395
|
+
warn(`${indent}${r.name}: ${r.detail}`);
|
|
161
396
|
else
|
|
162
|
-
fail(`${r.name}: ${r.detail}`);
|
|
397
|
+
fail(`${indent}${r.name}: ${r.detail}`);
|
|
163
398
|
if (r.fix)
|
|
164
|
-
say(dim(` → ${r.fix}`));
|
|
399
|
+
say(dim(` ${indent}→ ${r.fix}`));
|
|
165
400
|
}
|
|
166
401
|
const failed = results.filter((r) => r.status === 'fail').length;
|
|
167
402
|
say('');
|
package/dist/src/lib/ramx.d.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Nothing here holds a platform credential, and nothing logs a secret. The
|
|
6
6
|
* only credential sent to RAM/X is the RAM/X API key, as a Bearer header.
|
|
7
7
|
*/
|
|
8
|
-
export declare const BRIDGE_VERSION = "0.1.
|
|
9
|
-
export declare const USER_AGENT = "ramx-bridge/0.1.
|
|
8
|
+
export declare const BRIDGE_VERSION = "0.1.1";
|
|
9
|
+
export declare const USER_AGENT = "ramx-bridge/0.1.1";
|
|
10
10
|
export declare class RamxError extends Error {
|
|
11
11
|
readonly code: string;
|
|
12
12
|
readonly status: number;
|
package/dist/src/lib/ramx.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* only credential sent to RAM/X is the RAM/X API key, as a Bearer header.
|
|
7
7
|
*/
|
|
8
8
|
import { DEFAULT_API_BASE } from './config.js';
|
|
9
|
-
export const BRIDGE_VERSION = '0.1.
|
|
9
|
+
export const BRIDGE_VERSION = '0.1.1';
|
|
10
10
|
export const USER_AGENT = `ramx-bridge/${BRIDGE_VERSION}`;
|
|
11
11
|
export class RamxError extends Error {
|
|
12
12
|
code;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramxvnn/bridge",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "RAM/X Easy Connect
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "RAM/X Easy Connect — connect a bot to RAM/X without editing config files. Your platform credentials stay on your machine.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "RAM/X Foundation",
|
|
7
7
|
"homepage": "https://ramx.vn",
|