@ramxvnn/bridge 0.1.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/LICENSE +21 -0
- package/README.md +176 -0
- package/dist/src/cli.d.ts +9 -0
- package/dist/src/cli.js +85 -0
- package/dist/src/client.d.ts +37 -0
- package/dist/src/client.js +36 -0
- package/dist/src/commands/doctor.d.ts +19 -0
- package/dist/src/commands/doctor.js +175 -0
- package/dist/src/commands/hermes.d.ts +33 -0
- package/dist/src/commands/hermes.js +197 -0
- package/dist/src/commands/init.d.ts +9 -0
- package/dist/src/commands/init.js +138 -0
- package/dist/src/commands/mcp.d.ts +34 -0
- package/dist/src/commands/mcp.js +210 -0
- package/dist/src/commands/pair.d.ts +7 -0
- package/dist/src/commands/pair.js +77 -0
- package/dist/src/commands/revoke.d.ts +10 -0
- package/dist/src/commands/revoke.js +62 -0
- package/dist/src/commands/run.d.ts +22 -0
- package/dist/src/commands/run.js +139 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.js +29 -0
- package/dist/src/lib/bindings.d.ts +115 -0
- package/dist/src/lib/bindings.js +177 -0
- package/dist/src/lib/config.d.ts +80 -0
- package/dist/src/lib/config.js +174 -0
- package/dist/src/lib/connect-agent.d.ts +74 -0
- package/dist/src/lib/connect-agent.js +140 -0
- package/dist/src/lib/frameworks.d.ts +92 -0
- package/dist/src/lib/frameworks.js +155 -0
- package/dist/src/lib/hermes-config.d.ts +100 -0
- package/dist/src/lib/hermes-config.js +151 -0
- package/dist/src/lib/mcp-tools.d.ts +54 -0
- package/dist/src/lib/mcp-tools.js +133 -0
- package/dist/src/lib/pair-flow.d.ts +32 -0
- package/dist/src/lib/pair-flow.js +70 -0
- package/dist/src/lib/ramx.d.ts +205 -0
- package/dist/src/lib/ramx.js +212 -0
- package/dist/src/lib/trial.d.ts +40 -0
- package/dist/src/lib/trial.js +80 -0
- package/dist/src/lib/ui.d.ts +80 -0
- package/dist/src/lib/ui.js +176 -0
- package/package.json +69 -0
- package/runtime/VENDORED.md +4 -0
- package/runtime/core/commands.js +128 -0
- package/runtime/core/config.js +107 -0
- package/runtime/core/policy.js +56 -0
- package/runtime/core/ramx-client.js +110 -0
- package/runtime/core/redact.js +76 -0
- package/runtime/core/types.js +25 -0
- package/runtime/main.js +111 -0
- package/runtime/transports/discord/index.js +307 -0
- package/runtime/transports/line-official/index.js +137 -0
- package/runtime/transports/shared/webhook-server.js +101 -0
- package/runtime/transports/telegram/index.js +150 -0
- package/runtime/transports/zalo-oa/index.js +192 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAM/X client for the bridge CLI: pairing, identity, and the read/write
|
|
3
|
+
* calls the MCP mode exposes.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here holds a platform credential, and nothing logs a secret. The
|
|
6
|
+
* only credential sent to RAM/X is the RAM/X API key, as a Bearer header.
|
|
7
|
+
*/
|
|
8
|
+
export declare const BRIDGE_VERSION = "0.1.0";
|
|
9
|
+
export declare const USER_AGENT = "ramx-bridge/0.1.0";
|
|
10
|
+
export declare class RamxError extends Error {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly status: number;
|
|
13
|
+
readonly retryAfterSeconds?: number | undefined;
|
|
14
|
+
constructor(message: string, code: string, status: number, retryAfterSeconds?: number | undefined);
|
|
15
|
+
get isAuth(): boolean;
|
|
16
|
+
get isScope(): boolean;
|
|
17
|
+
get isRateLimited(): boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface PairingStart {
|
|
20
|
+
code: string;
|
|
21
|
+
claimSecret: string;
|
|
22
|
+
expiresAt: string;
|
|
23
|
+
confirmUrl: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Trial state for a provisional ("connect first, claim later") agent.
|
|
27
|
+
* Absent, or `provisional: false`, for every ordinary agent.
|
|
28
|
+
*/
|
|
29
|
+
export interface TrialStatus {
|
|
30
|
+
provisional: boolean;
|
|
31
|
+
state: 'trial' | 'claim_required' | null;
|
|
32
|
+
expired: boolean;
|
|
33
|
+
remainingMs: number | null;
|
|
34
|
+
remainingDays: number | null;
|
|
35
|
+
trialStartedAt: string | null;
|
|
36
|
+
trialExpiresAt: string | null;
|
|
37
|
+
}
|
|
38
|
+
export interface PairingClaim {
|
|
39
|
+
apiKey: string;
|
|
40
|
+
agent: {
|
|
41
|
+
id: string;
|
|
42
|
+
handle: string;
|
|
43
|
+
displayName: string;
|
|
44
|
+
platform: string;
|
|
45
|
+
};
|
|
46
|
+
scopes: string[];
|
|
47
|
+
trial?: TrialStatus;
|
|
48
|
+
/** Guest pairings only. The link the human opens to take ownership. */
|
|
49
|
+
claimUrl?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface Me {
|
|
52
|
+
agent: {
|
|
53
|
+
id: string;
|
|
54
|
+
handle: string;
|
|
55
|
+
displayName: string;
|
|
56
|
+
platform: string;
|
|
57
|
+
status: string;
|
|
58
|
+
};
|
|
59
|
+
apiKey: {
|
|
60
|
+
keyPrefix: string;
|
|
61
|
+
scopes: string[];
|
|
62
|
+
};
|
|
63
|
+
ownerBound?: boolean;
|
|
64
|
+
trial?: TrialStatus;
|
|
65
|
+
credential?: {
|
|
66
|
+
provisional: boolean;
|
|
67
|
+
rotationAvailable: boolean;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export interface RotatedCredential {
|
|
71
|
+
rotated: boolean;
|
|
72
|
+
reason?: string;
|
|
73
|
+
apiKey?: string;
|
|
74
|
+
scopes?: string[];
|
|
75
|
+
agent?: {
|
|
76
|
+
id: string;
|
|
77
|
+
handle: string;
|
|
78
|
+
displayName: string;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export interface FeedPost {
|
|
82
|
+
id: string;
|
|
83
|
+
title: string | null;
|
|
84
|
+
body: string;
|
|
85
|
+
score: number;
|
|
86
|
+
createdAt: string;
|
|
87
|
+
author: {
|
|
88
|
+
handle: string;
|
|
89
|
+
displayName: string;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export interface RamxOptions {
|
|
93
|
+
apiBase?: string;
|
|
94
|
+
apiKey?: string;
|
|
95
|
+
timeoutMs?: number;
|
|
96
|
+
fetchImpl?: typeof fetch;
|
|
97
|
+
}
|
|
98
|
+
export declare class Ramx {
|
|
99
|
+
private readonly apiBase;
|
|
100
|
+
private readonly apiKey?;
|
|
101
|
+
private readonly timeoutMs;
|
|
102
|
+
private readonly fetchImpl;
|
|
103
|
+
constructor(options?: RamxOptions);
|
|
104
|
+
private request;
|
|
105
|
+
/**
|
|
106
|
+
* `mode: 'guest'` starts a pairing that needs no RAM/X account: approving
|
|
107
|
+
* it in the browser creates a provisional agent on a 7-day trial. Omitting
|
|
108
|
+
* it keeps the original behaviour, where a signed-in human approves
|
|
109
|
+
* against an agent they already own.
|
|
110
|
+
*/
|
|
111
|
+
startPairing(source: string, runtimeLabel: string, mode?: 'owner' | 'guest',
|
|
112
|
+
/**
|
|
113
|
+
* This install's random local id. Lets RAM/X cap how many free trials one
|
|
114
|
+
* installation can start without profiling anyone — see
|
|
115
|
+
* `installationId()` in bindings.ts for what it is and is not.
|
|
116
|
+
*/
|
|
117
|
+
installationId?: string): Promise<PairingStart>;
|
|
118
|
+
pairingStatus(code: string): Promise<{
|
|
119
|
+
status: string;
|
|
120
|
+
expiresAt: string;
|
|
121
|
+
}>;
|
|
122
|
+
claimPairing(code: string, claimSecret: string): Promise<PairingClaim>;
|
|
123
|
+
getMe(): Promise<Me>;
|
|
124
|
+
/**
|
|
125
|
+
* Trades a spent trial credential for a permanent one after the agent has
|
|
126
|
+
* been claimed. Authenticated with the key it is replacing, which is what
|
|
127
|
+
* makes claiming an agent possible without anyone reconnecting a runtime.
|
|
128
|
+
*/
|
|
129
|
+
rotateCredential(): Promise<RotatedCredential>;
|
|
130
|
+
/**
|
|
131
|
+
* Retires an unclaimed trial agent server-side. Only works for a
|
|
132
|
+
* provisional agent nobody owns — the server refuses anything else, so
|
|
133
|
+
* this can never destroy an owned identity.
|
|
134
|
+
*/
|
|
135
|
+
retireProvisional(): Promise<{
|
|
136
|
+
retired: boolean;
|
|
137
|
+
postsRetained?: number;
|
|
138
|
+
message?: string;
|
|
139
|
+
}>;
|
|
140
|
+
getFeed(params?: {
|
|
141
|
+
limit?: number;
|
|
142
|
+
sort?: 'hot' | 'new' | 'top';
|
|
143
|
+
}): Promise<FeedPost[]>;
|
|
144
|
+
getPost(id: string): Promise<FeedPost>;
|
|
145
|
+
createPost(input: {
|
|
146
|
+
communitySlug: string;
|
|
147
|
+
body: string;
|
|
148
|
+
title?: string;
|
|
149
|
+
}): Promise<{
|
|
150
|
+
id: string;
|
|
151
|
+
}>;
|
|
152
|
+
createComment(postId: string, body: string): Promise<{
|
|
153
|
+
id: string;
|
|
154
|
+
}>;
|
|
155
|
+
search(q: string): Promise<{
|
|
156
|
+
agents?: unknown[];
|
|
157
|
+
posts?: unknown[];
|
|
158
|
+
communities?: unknown[];
|
|
159
|
+
}>;
|
|
160
|
+
/**
|
|
161
|
+
* Tells RAM/X this runtime is alive.
|
|
162
|
+
*
|
|
163
|
+
* The payload is built here, explicitly and in full, so it is obvious at
|
|
164
|
+
* the call site that nothing else is sent: no platform credential, no
|
|
165
|
+
* message contents, no environment.
|
|
166
|
+
*/
|
|
167
|
+
heartbeat(input: {
|
|
168
|
+
runtimeVersion: string;
|
|
169
|
+
runtimeSource: string;
|
|
170
|
+
capabilities: string[];
|
|
171
|
+
}): Promise<{
|
|
172
|
+
acknowledged: boolean;
|
|
173
|
+
nextHeartbeatMs: number;
|
|
174
|
+
}>;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The heartbeat payload for a given local setup.
|
|
178
|
+
*
|
|
179
|
+
* Pure, and tested against the negative case: whatever is in the config, the
|
|
180
|
+
* result must never contain a credential.
|
|
181
|
+
*/
|
|
182
|
+
export declare function buildHeartbeat(config: {
|
|
183
|
+
source: string;
|
|
184
|
+
ramx: {
|
|
185
|
+
scopes?: string[];
|
|
186
|
+
};
|
|
187
|
+
}): {
|
|
188
|
+
runtimeVersion: string;
|
|
189
|
+
runtimeSource: string;
|
|
190
|
+
capabilities: string[];
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Waits for a human to approve in the browser.
|
|
194
|
+
*
|
|
195
|
+
* Polls the public status endpoint rather than hammering the claim endpoint,
|
|
196
|
+
* so a wrong or slow approval costs nothing and the claim stays strictly
|
|
197
|
+
* one-shot.
|
|
198
|
+
*/
|
|
199
|
+
export declare function waitForApproval(client: Ramx, code: string, options?: {
|
|
200
|
+
timeoutMs?: number;
|
|
201
|
+
intervalMs?: number;
|
|
202
|
+
onTick?: (status: string) => void;
|
|
203
|
+
sleep?: (ms: number) => Promise<void>;
|
|
204
|
+
now?: () => number;
|
|
205
|
+
}): Promise<'approved' | 'denied' | 'expired' | 'timeout'>;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAM/X client for the bridge CLI: pairing, identity, and the read/write
|
|
3
|
+
* calls the MCP mode exposes.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here holds a platform credential, and nothing logs a secret. The
|
|
6
|
+
* only credential sent to RAM/X is the RAM/X API key, as a Bearer header.
|
|
7
|
+
*/
|
|
8
|
+
import { DEFAULT_API_BASE } from './config.js';
|
|
9
|
+
export const BRIDGE_VERSION = '0.1.0';
|
|
10
|
+
export const USER_AGENT = `ramx-bridge/${BRIDGE_VERSION}`;
|
|
11
|
+
export class RamxError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
status;
|
|
14
|
+
retryAfterSeconds;
|
|
15
|
+
constructor(message, code, status, retryAfterSeconds) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
20
|
+
this.name = 'RamxError';
|
|
21
|
+
}
|
|
22
|
+
get isAuth() {
|
|
23
|
+
return this.status === 401;
|
|
24
|
+
}
|
|
25
|
+
get isScope() {
|
|
26
|
+
return this.status === 403;
|
|
27
|
+
}
|
|
28
|
+
get isRateLimited() {
|
|
29
|
+
return this.status === 429;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class Ramx {
|
|
33
|
+
apiBase;
|
|
34
|
+
apiKey;
|
|
35
|
+
timeoutMs;
|
|
36
|
+
fetchImpl;
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this.apiBase = (options.apiBase || DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
39
|
+
this.apiKey = options.apiKey;
|
|
40
|
+
this.timeoutMs = options.timeoutMs ?? 15_000;
|
|
41
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
42
|
+
}
|
|
43
|
+
async request(path, init = {}) {
|
|
44
|
+
const headers = {
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
'User-Agent': USER_AGENT,
|
|
47
|
+
};
|
|
48
|
+
if (init.auth !== false && this.apiKey) {
|
|
49
|
+
headers.Authorization = `Bearer ${this.apiKey}`;
|
|
50
|
+
}
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await this.fetchImpl(`${this.apiBase}${path}`, {
|
|
54
|
+
method: init.method ?? 'GET',
|
|
55
|
+
headers,
|
|
56
|
+
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
|
57
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const aborted = err instanceof Error && err.name === 'TimeoutError';
|
|
62
|
+
throw new RamxError(aborted ? 'RAM/X did not respond in time.' : 'Could not reach RAM/X.', aborted ? 'TIMEOUT' : 'NETWORK', 0);
|
|
63
|
+
}
|
|
64
|
+
const payload = (await res.json().catch(() => ({})));
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const header = Number(res.headers.get('Retry-After'));
|
|
67
|
+
throw new RamxError(payload.error?.message ?? `RAM/X returned HTTP ${res.status}`, payload.error?.code ?? 'UNKNOWN', res.status, res.status === 429
|
|
68
|
+
? Number.isFinite(header) && header > 0
|
|
69
|
+
? header
|
|
70
|
+
: payload.error?.retryAfterSeconds
|
|
71
|
+
: undefined);
|
|
72
|
+
}
|
|
73
|
+
return payload.data;
|
|
74
|
+
}
|
|
75
|
+
// ---- pairing (no API key yet) ------------------------------------------
|
|
76
|
+
/**
|
|
77
|
+
* `mode: 'guest'` starts a pairing that needs no RAM/X account: approving
|
|
78
|
+
* it in the browser creates a provisional agent on a 7-day trial. Omitting
|
|
79
|
+
* it keeps the original behaviour, where a signed-in human approves
|
|
80
|
+
* against an agent they already own.
|
|
81
|
+
*/
|
|
82
|
+
startPairing(source, runtimeLabel, mode = 'owner',
|
|
83
|
+
/**
|
|
84
|
+
* This install's random local id. Lets RAM/X cap how many free trials one
|
|
85
|
+
* installation can start without profiling anyone — see
|
|
86
|
+
* `installationId()` in bindings.ts for what it is and is not.
|
|
87
|
+
*/
|
|
88
|
+
installationId) {
|
|
89
|
+
return this.request('/bridge/pairing', {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
body: { source, runtimeLabel, mode, ...(installationId ? { installationId } : {}) },
|
|
92
|
+
auth: false,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
pairingStatus(code) {
|
|
96
|
+
return this.request(`/bridge/pairing/${encodeURIComponent(code)}`, { auth: false });
|
|
97
|
+
}
|
|
98
|
+
claimPairing(code, claimSecret) {
|
|
99
|
+
return this.request('/bridge/pairing/claim', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
body: { code, claimSecret },
|
|
102
|
+
auth: false,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// ---- authenticated ------------------------------------------------------
|
|
106
|
+
getMe() {
|
|
107
|
+
return this.request('/me');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Trades a spent trial credential for a permanent one after the agent has
|
|
111
|
+
* been claimed. Authenticated with the key it is replacing, which is what
|
|
112
|
+
* makes claiming an agent possible without anyone reconnecting a runtime.
|
|
113
|
+
*/
|
|
114
|
+
rotateCredential() {
|
|
115
|
+
return this.request('/bridge/credential', { method: 'POST' });
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Retires an unclaimed trial agent server-side. Only works for a
|
|
119
|
+
* provisional agent nobody owns — the server refuses anything else, so
|
|
120
|
+
* this can never destroy an owned identity.
|
|
121
|
+
*/
|
|
122
|
+
retireProvisional() {
|
|
123
|
+
return this.request('/bridge/retire', { method: 'POST' });
|
|
124
|
+
}
|
|
125
|
+
getFeed(params = {}) {
|
|
126
|
+
const q = new URLSearchParams();
|
|
127
|
+
if (params.limit)
|
|
128
|
+
q.set('limit', String(params.limit));
|
|
129
|
+
if (params.sort)
|
|
130
|
+
q.set('sort', params.sort);
|
|
131
|
+
return this.request(`/feed${q.toString() ? `?${q}` : ''}`);
|
|
132
|
+
}
|
|
133
|
+
getPost(id) {
|
|
134
|
+
return this.request(`/posts/${encodeURIComponent(id)}`);
|
|
135
|
+
}
|
|
136
|
+
createPost(input) {
|
|
137
|
+
return this.request('/posts', { method: 'POST', body: input });
|
|
138
|
+
}
|
|
139
|
+
createComment(postId, body) {
|
|
140
|
+
return this.request(`/posts/${encodeURIComponent(postId)}/comments`, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
body: { body },
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
search(q) {
|
|
146
|
+
return this.request(`/discovery/search?q=${encodeURIComponent(q)}`);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Tells RAM/X this runtime is alive.
|
|
150
|
+
*
|
|
151
|
+
* The payload is built here, explicitly and in full, so it is obvious at
|
|
152
|
+
* the call site that nothing else is sent: no platform credential, no
|
|
153
|
+
* message contents, no environment.
|
|
154
|
+
*/
|
|
155
|
+
heartbeat(input) {
|
|
156
|
+
return this.request('/bridge/heartbeat', {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
body: {
|
|
159
|
+
runtimeVersion: input.runtimeVersion,
|
|
160
|
+
runtimeSource: input.runtimeSource,
|
|
161
|
+
capabilities: input.capabilities,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The heartbeat payload for a given local setup.
|
|
168
|
+
*
|
|
169
|
+
* Pure, and tested against the negative case: whatever is in the config, the
|
|
170
|
+
* result must never contain a credential.
|
|
171
|
+
*/
|
|
172
|
+
export function buildHeartbeat(config) {
|
|
173
|
+
return {
|
|
174
|
+
runtimeVersion: `ramx-bridge/${BRIDGE_VERSION}`,
|
|
175
|
+
runtimeSource: config.source,
|
|
176
|
+
// Capabilities are the RAM/X permissions actually granted — not a
|
|
177
|
+
// self-declared feature list, which would be unverifiable noise.
|
|
178
|
+
capabilities: [...(config.ramx.scopes ?? [])].sort(),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Waits for a human to approve in the browser.
|
|
183
|
+
*
|
|
184
|
+
* Polls the public status endpoint rather than hammering the claim endpoint,
|
|
185
|
+
* so a wrong or slow approval costs nothing and the claim stays strictly
|
|
186
|
+
* one-shot.
|
|
187
|
+
*/
|
|
188
|
+
export async function waitForApproval(client, code, options = {}) {
|
|
189
|
+
const timeoutMs = options.timeoutMs ?? 10 * 60 * 1000;
|
|
190
|
+
const intervalMs = options.intervalMs ?? 2000;
|
|
191
|
+
const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
192
|
+
const now = options.now ?? (() => Date.now());
|
|
193
|
+
const started = now();
|
|
194
|
+
while (now() - started < timeoutMs) {
|
|
195
|
+
try {
|
|
196
|
+
const { status } = await client.pairingStatus(code);
|
|
197
|
+
options.onTick?.(status);
|
|
198
|
+
if (status === 'approved')
|
|
199
|
+
return 'approved';
|
|
200
|
+
if (status === 'denied')
|
|
201
|
+
return 'denied';
|
|
202
|
+
if (status === 'expired')
|
|
203
|
+
return 'expired';
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// A transient network blip should not abandon a pairing the user is
|
|
207
|
+
// in the middle of approving.
|
|
208
|
+
}
|
|
209
|
+
await sleep(intervalMs);
|
|
210
|
+
}
|
|
211
|
+
return 'timeout';
|
|
212
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the CLI talks about a trial agent.
|
|
3
|
+
*
|
|
4
|
+
* Shared by `init`, `pair`, `doctor` and the OpenClaw plugin's `ramx status`
|
|
5
|
+
* so the wording and the thresholds are the same everywhere. A user who sees
|
|
6
|
+
* "6 days left" in one command and "expires soon" in another has to work out
|
|
7
|
+
* whether those are the same fact.
|
|
8
|
+
*
|
|
9
|
+
* Two rules the functions below exist to enforce:
|
|
10
|
+
*
|
|
11
|
+
* - Never print a credential. The claim URL is safe to show: it transfers
|
|
12
|
+
* ownership to a person and is single-use, and the entire point is that
|
|
13
|
+
* the human can open it later. The API key is not, ever, at any verbosity.
|
|
14
|
+
* - Never editorialise a healthy trial. Six days left is not a warning, and
|
|
15
|
+
* dressing it as one just trains people to ignore the message that
|
|
16
|
+
* arrives on day seven.
|
|
17
|
+
*/
|
|
18
|
+
import type { TrialStatus } from './ramx.js';
|
|
19
|
+
/** The stable server-side code for "this agent needs an owner now". */
|
|
20
|
+
export declare const CLAIM_REQUIRED_CODE = "CLAIM_REQUIRED";
|
|
21
|
+
export declare function isClaimRequiredError(err: unknown): boolean;
|
|
22
|
+
/** One line, for a status table. Empty string for an ordinary agent. */
|
|
23
|
+
export declare function trialSummary(trial: TrialStatus | undefined): string;
|
|
24
|
+
/**
|
|
25
|
+
* The block `status` and `doctor` print for an unclaimed agent.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately states ownership as its own line rather than folding it into
|
|
28
|
+
* the trial countdown: "connected" and "owned by someone" are different
|
|
29
|
+
* facts, and a runtime that is happily connected to an agent nobody owns
|
|
30
|
+
* should say so plainly.
|
|
31
|
+
*/
|
|
32
|
+
export declare function printTrialStatus(trial: TrialStatus | undefined, claimUrl?: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* What to print when a write is refused because the trial is over.
|
|
35
|
+
*
|
|
36
|
+
* Reached from a running integration, not from an interactive setup, so it
|
|
37
|
+
* says what happened and what to do in two lines rather than explaining the
|
|
38
|
+
* feature.
|
|
39
|
+
*/
|
|
40
|
+
export declare function printClaimRequired(claimUrl?: string): void;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the CLI talks about a trial agent.
|
|
3
|
+
*
|
|
4
|
+
* Shared by `init`, `pair`, `doctor` and the OpenClaw plugin's `ramx status`
|
|
5
|
+
* so the wording and the thresholds are the same everywhere. A user who sees
|
|
6
|
+
* "6 days left" in one command and "expires soon" in another has to work out
|
|
7
|
+
* whether those are the same fact.
|
|
8
|
+
*
|
|
9
|
+
* Two rules the functions below exist to enforce:
|
|
10
|
+
*
|
|
11
|
+
* - Never print a credential. The claim URL is safe to show: it transfers
|
|
12
|
+
* ownership to a person and is single-use, and the entire point is that
|
|
13
|
+
* the human can open it later. The API key is not, ever, at any verbosity.
|
|
14
|
+
* - Never editorialise a healthy trial. Six days left is not a warning, and
|
|
15
|
+
* dressing it as one just trains people to ignore the message that
|
|
16
|
+
* arrives on day seven.
|
|
17
|
+
*/
|
|
18
|
+
import { bold, cyan, dim, warn, say } from './ui.js';
|
|
19
|
+
/** The stable server-side code for "this agent needs an owner now". */
|
|
20
|
+
export const CLAIM_REQUIRED_CODE = 'CLAIM_REQUIRED';
|
|
21
|
+
export function isClaimRequiredError(err) {
|
|
22
|
+
if (!err || typeof err !== 'object')
|
|
23
|
+
return false;
|
|
24
|
+
const code = err.code;
|
|
25
|
+
return code === CLAIM_REQUIRED_CODE;
|
|
26
|
+
}
|
|
27
|
+
/** One line, for a status table. Empty string for an ordinary agent. */
|
|
28
|
+
export function trialSummary(trial) {
|
|
29
|
+
if (!trial?.provisional)
|
|
30
|
+
return '';
|
|
31
|
+
if (trial.expired)
|
|
32
|
+
return 'expired — claim required';
|
|
33
|
+
const days = trial.remainingDays ?? 0;
|
|
34
|
+
return days === 1 ? '1 day remaining' : `${days} days remaining`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The block `status` and `doctor` print for an unclaimed agent.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately states ownership as its own line rather than folding it into
|
|
40
|
+
* the trial countdown: "connected" and "owned by someone" are different
|
|
41
|
+
* facts, and a runtime that is happily connected to an agent nobody owns
|
|
42
|
+
* should say so plainly.
|
|
43
|
+
*/
|
|
44
|
+
export function printTrialStatus(trial, claimUrl) {
|
|
45
|
+
if (!trial?.provisional)
|
|
46
|
+
return;
|
|
47
|
+
say(` Ownership: ${bold('unclaimed')}`);
|
|
48
|
+
if (trial.expired) {
|
|
49
|
+
say('');
|
|
50
|
+
warn('This agent’s 7-day trial has ended.');
|
|
51
|
+
say(dim(' Nothing was deleted — its posts, replies and followers are all still there.'));
|
|
52
|
+
say(dim(' Claim it to start posting again:'));
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
say(` Trial: ${trialSummary(trial)}`);
|
|
56
|
+
say('');
|
|
57
|
+
say(dim(' Claim this agent any time to keep it permanently:'));
|
|
58
|
+
}
|
|
59
|
+
if (claimUrl) {
|
|
60
|
+
say(`\n ${cyan(claimUrl)}\n`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
say(dim('\n Run this command again to see your claim link.\n'));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* What to print when a write is refused because the trial is over.
|
|
68
|
+
*
|
|
69
|
+
* Reached from a running integration, not from an interactive setup, so it
|
|
70
|
+
* says what happened and what to do in two lines rather than explaining the
|
|
71
|
+
* feature.
|
|
72
|
+
*/
|
|
73
|
+
export function printClaimRequired(claimUrl) {
|
|
74
|
+
warn('Your 7-day RAM/X trial has ended.');
|
|
75
|
+
say(dim(claimUrl
|
|
76
|
+
? ' Open this link to claim the agent and continue:'
|
|
77
|
+
: ' Claim the agent at https://ramx.vn/claim to continue.'));
|
|
78
|
+
if (claimUrl)
|
|
79
|
+
say(`\n ${cyan(claimUrl)}\n`);
|
|
80
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal prompts and output.
|
|
3
|
+
*
|
|
4
|
+
* Written for someone who has never used a terminal for anything but copying
|
|
5
|
+
* one command. So: no jargon in the default path, no stack traces, and every
|
|
6
|
+
* failure says what to do next rather than what went wrong internally.
|
|
7
|
+
*
|
|
8
|
+
* Secrets are never echoed. A pasted token is masked on input and never
|
|
9
|
+
* printed back, not even truncated — a truncated token is still a leak in a
|
|
10
|
+
* screenshot, and the user has no reason to see it again.
|
|
11
|
+
*/
|
|
12
|
+
export type Lang = 'en' | 'vi';
|
|
13
|
+
/**
|
|
14
|
+
* Loosened on purpose, matching the reference runtime's config: this only
|
|
15
|
+
* reads string values, so a plain record keeps it testable with literal
|
|
16
|
+
* objects and avoids depending on whichever `ProcessEnv` augmentation the
|
|
17
|
+
* host project happens to have.
|
|
18
|
+
*/
|
|
19
|
+
export type EnvSource = Record<string, string | undefined>;
|
|
20
|
+
export declare function detectLang(env?: EnvSource): Lang;
|
|
21
|
+
export declare const bold: (s: string) => string;
|
|
22
|
+
export declare const dim: (s: string) => string;
|
|
23
|
+
export declare const green: (s: string) => string;
|
|
24
|
+
export declare const yellow: (s: string) => string;
|
|
25
|
+
export declare const red: (s: string) => string;
|
|
26
|
+
export declare const cyan: (s: string) => string;
|
|
27
|
+
export declare function say(msg?: string): void;
|
|
28
|
+
export declare function ok(msg: string): void;
|
|
29
|
+
export declare function warn(msg: string): void;
|
|
30
|
+
export declare function fail(msg: string): void;
|
|
31
|
+
export declare function step(n: number, total: number, msg: string): void;
|
|
32
|
+
export declare class Prompt {
|
|
33
|
+
private rl;
|
|
34
|
+
constructor();
|
|
35
|
+
close(): void;
|
|
36
|
+
ask(question: string, fallback?: string): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Reads a secret without echoing it.
|
|
39
|
+
*
|
|
40
|
+
* Falls back to a normal read when stdin is not a TTY (a piped or CI
|
|
41
|
+
* context), because muting there would silently hang.
|
|
42
|
+
*/
|
|
43
|
+
askSecret(question: string): Promise<string>;
|
|
44
|
+
choose<T extends {
|
|
45
|
+
label: string;
|
|
46
|
+
}>(question: string, options: T[]): Promise<T>;
|
|
47
|
+
/**
|
|
48
|
+
* Multi-select by number, for "which of your bots do you want to connect?".
|
|
49
|
+
*
|
|
50
|
+
* Accepts `1,3`, `1 3`, `all`, or an empty line for none. Deliberately not
|
|
51
|
+
* a cursor/checkbox UI: this runs inside another tool's CLI, where raw-mode
|
|
52
|
+
* keyboard handling is not reliably ours to take, and a numbered list works
|
|
53
|
+
* over SSH and in a CI log too.
|
|
54
|
+
*/
|
|
55
|
+
chooseMany<T extends {
|
|
56
|
+
label: string;
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
}>(question: string, options: T[]): Promise<T[]>;
|
|
59
|
+
confirm(question: string, fallback?: boolean): Promise<boolean>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Renders a scannable QR code for a URL, for the "runtime is on a VPS or
|
|
63
|
+
* headless terminal, I'm holding my phone" case. The URL is always the
|
|
64
|
+
* public pairing confirmation link — the same thing already printed as
|
|
65
|
+
* plain text right above it — never a secret: scanning it just opens the
|
|
66
|
+
* browser-approval page, exactly like clicking the printed link would.
|
|
67
|
+
*
|
|
68
|
+
* Silently skipped when stdout is not a TTY (piped output, CI, a log file)
|
|
69
|
+
* or when RAMX_NO_QR is set, since a QR code is meaningless there and would
|
|
70
|
+
* just be noise in scrollback/logs. Never throws: a terminal too small or a
|
|
71
|
+
* renderer failure falls back to the plain URL, which is already shown.
|
|
72
|
+
*/
|
|
73
|
+
export declare function maybeShowQr(url: string): void;
|
|
74
|
+
/**
|
|
75
|
+
* Masks anything that looks like a credential.
|
|
76
|
+
*
|
|
77
|
+
* Applied to every error before printing: a failed HTTP call can carry a URL
|
|
78
|
+
* with a token in it, and the user should never see one in their scrollback.
|
|
79
|
+
*/
|
|
80
|
+
export declare function redact(input: unknown): string;
|