@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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/dist/src/cli.d.ts +9 -0
  4. package/dist/src/cli.js +85 -0
  5. package/dist/src/client.d.ts +37 -0
  6. package/dist/src/client.js +36 -0
  7. package/dist/src/commands/doctor.d.ts +19 -0
  8. package/dist/src/commands/doctor.js +175 -0
  9. package/dist/src/commands/hermes.d.ts +33 -0
  10. package/dist/src/commands/hermes.js +197 -0
  11. package/dist/src/commands/init.d.ts +9 -0
  12. package/dist/src/commands/init.js +138 -0
  13. package/dist/src/commands/mcp.d.ts +34 -0
  14. package/dist/src/commands/mcp.js +210 -0
  15. package/dist/src/commands/pair.d.ts +7 -0
  16. package/dist/src/commands/pair.js +77 -0
  17. package/dist/src/commands/revoke.d.ts +10 -0
  18. package/dist/src/commands/revoke.js +62 -0
  19. package/dist/src/commands/run.d.ts +22 -0
  20. package/dist/src/commands/run.js +139 -0
  21. package/dist/src/index.d.ts +20 -0
  22. package/dist/src/index.js +29 -0
  23. package/dist/src/lib/bindings.d.ts +115 -0
  24. package/dist/src/lib/bindings.js +177 -0
  25. package/dist/src/lib/config.d.ts +80 -0
  26. package/dist/src/lib/config.js +174 -0
  27. package/dist/src/lib/connect-agent.d.ts +74 -0
  28. package/dist/src/lib/connect-agent.js +140 -0
  29. package/dist/src/lib/frameworks.d.ts +92 -0
  30. package/dist/src/lib/frameworks.js +155 -0
  31. package/dist/src/lib/hermes-config.d.ts +100 -0
  32. package/dist/src/lib/hermes-config.js +151 -0
  33. package/dist/src/lib/mcp-tools.d.ts +54 -0
  34. package/dist/src/lib/mcp-tools.js +133 -0
  35. package/dist/src/lib/pair-flow.d.ts +32 -0
  36. package/dist/src/lib/pair-flow.js +70 -0
  37. package/dist/src/lib/ramx.d.ts +205 -0
  38. package/dist/src/lib/ramx.js +212 -0
  39. package/dist/src/lib/trial.d.ts +40 -0
  40. package/dist/src/lib/trial.js +80 -0
  41. package/dist/src/lib/ui.d.ts +80 -0
  42. package/dist/src/lib/ui.js +176 -0
  43. package/package.json +69 -0
  44. package/runtime/VENDORED.md +4 -0
  45. package/runtime/core/commands.js +128 -0
  46. package/runtime/core/config.js +107 -0
  47. package/runtime/core/policy.js +56 -0
  48. package/runtime/core/ramx-client.js +110 -0
  49. package/runtime/core/redact.js +76 -0
  50. package/runtime/core/types.js +25 -0
  51. package/runtime/main.js +111 -0
  52. package/runtime/transports/discord/index.js +307 -0
  53. package/runtime/transports/line-official/index.js +137 -0
  54. package/runtime/transports/shared/webhook-server.js +101 -0
  55. package/runtime/transports/telegram/index.js +150 -0
  56. 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;