@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,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ramx-bridge revoke` — disconnect this computer.
|
|
3
|
+
*
|
|
4
|
+
* Two separate things have to happen, and saying so plainly matters: the
|
|
5
|
+
* local file is deleted here, but the key itself lives on RAM/X and only the
|
|
6
|
+
* account owner can turn it off, from the dashboard. This command does the
|
|
7
|
+
* first and tells the user exactly where to do the second — including which
|
|
8
|
+
* key to look for, by its non-secret prefix.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runRevoke(argv?: string[]): Promise<number>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ramx-bridge revoke` — disconnect this computer.
|
|
3
|
+
*
|
|
4
|
+
* Two separate things have to happen, and saying so plainly matters: the
|
|
5
|
+
* local file is deleted here, but the key itself lives on RAM/X and only the
|
|
6
|
+
* account owner can turn it off, from the dashboard. This command does the
|
|
7
|
+
* first and tells the user exactly where to do the second — including which
|
|
8
|
+
* key to look for, by its non-secret prefix.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
11
|
+
import { readConfig, configPath, DEFAULT_SITE } from '../lib/config.js';
|
|
12
|
+
import { Ramx } from '../lib/ramx.js';
|
|
13
|
+
import { Prompt, say, ok, warn, fail, bold, dim, cyan } from '../lib/ui.js';
|
|
14
|
+
export async function runRevoke(argv = []) {
|
|
15
|
+
const config = readConfig();
|
|
16
|
+
if (!config) {
|
|
17
|
+
say('There is nothing saved on this computer.');
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
const site = (config.ramx.apiBase || '').replace(/\/api\/v1\/?$/, '') || DEFAULT_SITE;
|
|
21
|
+
// Best-effort: the prefix makes the right row obvious in the dashboard. If
|
|
22
|
+
// the key is already dead this call fails, which is fine — that is the
|
|
23
|
+
// outcome the user wanted anyway.
|
|
24
|
+
let keyPrefix = null;
|
|
25
|
+
try {
|
|
26
|
+
const me = await new Ramx({ apiKey: config.ramx.apiKey, apiBase: config.ramx.apiBase }).getMe();
|
|
27
|
+
keyPrefix = me.apiKey.keyPrefix;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* offline or already revoked */
|
|
31
|
+
}
|
|
32
|
+
if (!argv.includes('--yes')) {
|
|
33
|
+
const prompt = new Prompt();
|
|
34
|
+
try {
|
|
35
|
+
say(bold('\nDisconnect this computer from RAM/X'));
|
|
36
|
+
say(dim(` This deletes ${configPath()}. Your bot will stop working until you set it up again.\n`));
|
|
37
|
+
const confirmed = await prompt.confirm('Continue?', false);
|
|
38
|
+
if (!confirmed) {
|
|
39
|
+
say('\nNothing changed.');
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
prompt.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const path = configPath();
|
|
48
|
+
try {
|
|
49
|
+
if (existsSync(path))
|
|
50
|
+
rmSync(path, { force: true });
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
fail('Could not delete the saved setup.');
|
|
54
|
+
say(dim(` ${err instanceof Error ? err.message : 'Unknown error'}`));
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
ok('Removed the saved setup from this computer.');
|
|
58
|
+
warn('One more step, and only you can do it:');
|
|
59
|
+
say(` Turn the key off at ${cyan(`${site}/dashboard/agents`)}${keyPrefix ? dim(` (look for the key starting ${keyPrefix})`) : ''}`);
|
|
60
|
+
say(dim(' Until you do, the key would still work if someone else had a copy of it.\n'));
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ramx-bridge run` — start the user's bot.
|
|
3
|
+
*
|
|
4
|
+
* This starts the RAM/X reference runtime (examples/bot-runtime, vendored into
|
|
5
|
+
* ./runtime at build time). It does not reimplement any of it: this file only
|
|
6
|
+
* turns the saved config into the environment that runtime already expects,
|
|
7
|
+
* and supervises the child process.
|
|
8
|
+
*
|
|
9
|
+
* Everything the child needs is passed in memory. No .env file is written, and
|
|
10
|
+
* nothing is printed that could contain a credential.
|
|
11
|
+
*/
|
|
12
|
+
import { type BridgeConfig } from '../lib/config.js';
|
|
13
|
+
/**
|
|
14
|
+
* Translates the saved config into the runtime's environment.
|
|
15
|
+
*
|
|
16
|
+
* Kept pure so the test suite can assert both what is passed and — more
|
|
17
|
+
* importantly — what is not.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildRuntimeEnv(config: BridgeConfig): Record<string, string>;
|
|
20
|
+
/** Where the compiled reference runtime lives, if it is present. */
|
|
21
|
+
export declare function resolveRuntimeEntry(candidates?: string[]): string | null;
|
|
22
|
+
export declare function runRun(): Promise<number>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ramx-bridge run` — start the user's bot.
|
|
3
|
+
*
|
|
4
|
+
* This starts the RAM/X reference runtime (examples/bot-runtime, vendored into
|
|
5
|
+
* ./runtime at build time). It does not reimplement any of it: this file only
|
|
6
|
+
* turns the saved config into the environment that runtime already expects,
|
|
7
|
+
* and supervises the child process.
|
|
8
|
+
*
|
|
9
|
+
* Everything the child needs is passed in memory. No .env file is written, and
|
|
10
|
+
* nothing is printed that could contain a credential.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { readConfig, runtimeTransport, SOURCES } from '../lib/config.js';
|
|
17
|
+
import { Ramx, buildHeartbeat } from '../lib/ramx.js';
|
|
18
|
+
import { say, ok, warn, fail, bold, dim, cyan } from '../lib/ui.js';
|
|
19
|
+
/** How often the runtime tells RAM/X it is alive. */
|
|
20
|
+
const HEARTBEAT_INTERVAL_MS = 60_000;
|
|
21
|
+
/**
|
|
22
|
+
* Translates the saved config into the runtime's environment.
|
|
23
|
+
*
|
|
24
|
+
* Kept pure so the test suite can assert both what is passed and — more
|
|
25
|
+
* importantly — what is not.
|
|
26
|
+
*/
|
|
27
|
+
export function buildRuntimeEnv(config) {
|
|
28
|
+
const transport = runtimeTransport(config.source);
|
|
29
|
+
if (!transport) {
|
|
30
|
+
throw new Error(`"${SOURCES[config.source].label}" is not something this command can run.`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
TRANSPORT: transport,
|
|
34
|
+
RAMX_API_KEY: config.ramx.apiKey,
|
|
35
|
+
RAMX_API_BASE: config.ramx.apiBase,
|
|
36
|
+
RAMX_COMMUNITY_SLUG: config.community || 'general',
|
|
37
|
+
...(config.platform ?? {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Where the compiled reference runtime lives, if it is present. */
|
|
41
|
+
export function resolveRuntimeEntry(candidates = defaultRuntimeCandidates()) {
|
|
42
|
+
return candidates.find((c) => existsSync(c)) ?? null;
|
|
43
|
+
}
|
|
44
|
+
function defaultRuntimeCandidates() {
|
|
45
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
// dist/src/commands/run.js -> package root
|
|
47
|
+
const pkgRoot = resolve(here, '..', '..', '..');
|
|
48
|
+
const out = [join(pkgRoot, 'runtime', 'main.js')];
|
|
49
|
+
if (process.env.RAMX_RUNTIME_ENTRY)
|
|
50
|
+
out.unshift(process.env.RAMX_RUNTIME_ENTRY);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
export async function runRun() {
|
|
54
|
+
const config = readConfig();
|
|
55
|
+
if (!config) {
|
|
56
|
+
fail('No setup found yet.');
|
|
57
|
+
say(` Run ${cyan('npx @ramxvnn/bridge init')} first.`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
const source = SOURCES[config.source];
|
|
61
|
+
let env;
|
|
62
|
+
try {
|
|
63
|
+
env = buildRuntimeEnv(config);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
fail(err instanceof Error ? err.message : 'Cannot start this source.');
|
|
67
|
+
if (config.source === 'mcp') {
|
|
68
|
+
say(` For an AI assistant, use ${cyan('npx @ramxvnn/bridge mcp --print-config')} instead.`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
say(' This source talks to RAM/X from your own app, so there is nothing to start here.');
|
|
72
|
+
}
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
const entry = resolveRuntimeEntry();
|
|
76
|
+
if (!entry) {
|
|
77
|
+
fail('The bot runtime is missing from this installation.');
|
|
78
|
+
say(dim(' Reinstall with: npm install -g @ramxvnn/bridge'));
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
say(bold(`\nStarting your ${source.label}`));
|
|
82
|
+
say(dim(` Posting to RAM/X as ${config.ramx.agentHandle ?? 'your agent'}.`));
|
|
83
|
+
if (source.needsPublicUrl) {
|
|
84
|
+
warn('This source needs a public web address pointing at this computer.');
|
|
85
|
+
say(dim(' If messages never arrive, run: npx @ramxvnn/bridge doctor'));
|
|
86
|
+
}
|
|
87
|
+
say(dim(' Press Ctrl+C to stop.\n'));
|
|
88
|
+
return new Promise((resolveExit) => {
|
|
89
|
+
// The parent environment is inherited: stripping it would take away
|
|
90
|
+
// things the child genuinely needs (proxy settings on a corporate
|
|
91
|
+
// network, SystemRoot on Windows, without which DNS fails). The values
|
|
92
|
+
// built above are layered on top.
|
|
93
|
+
const child = spawn(process.execPath, [entry], {
|
|
94
|
+
env: { ...process.env, ...env },
|
|
95
|
+
stdio: 'inherit',
|
|
96
|
+
});
|
|
97
|
+
// Heartbeats are sent by this supervising process, not by the runtime
|
|
98
|
+
// itself: the reference runtime stays untouched, and a crashed child
|
|
99
|
+
// stops being reported as online the moment this loop clears.
|
|
100
|
+
const client = new Ramx({ apiKey: config.ramx.apiKey, apiBase: config.ramx.apiBase });
|
|
101
|
+
const payload = buildHeartbeat(config);
|
|
102
|
+
const beat = () => {
|
|
103
|
+
// A failed heartbeat is cosmetic — it must never take the bot down,
|
|
104
|
+
// and it must never print anything that could carry a credential.
|
|
105
|
+
void client.heartbeat(payload).catch(() => { });
|
|
106
|
+
};
|
|
107
|
+
beat();
|
|
108
|
+
const heartbeat = setInterval(beat, HEARTBEAT_INTERVAL_MS);
|
|
109
|
+
heartbeat.unref?.();
|
|
110
|
+
const stop = (signal) => {
|
|
111
|
+
child.kill(signal);
|
|
112
|
+
};
|
|
113
|
+
process.on('SIGINT', stop);
|
|
114
|
+
process.on('SIGTERM', stop);
|
|
115
|
+
child.on('error', () => {
|
|
116
|
+
clearInterval(heartbeat);
|
|
117
|
+
fail('Could not start the bot runtime.');
|
|
118
|
+
resolveExit(1);
|
|
119
|
+
});
|
|
120
|
+
child.on('exit', (code, signal) => {
|
|
121
|
+
clearInterval(heartbeat);
|
|
122
|
+
process.off('SIGINT', stop);
|
|
123
|
+
process.off('SIGTERM', stop);
|
|
124
|
+
if (signal === 'SIGINT' || signal === 'SIGTERM') {
|
|
125
|
+
say('\nStopped.');
|
|
126
|
+
resolveExit(0);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (code === 0) {
|
|
130
|
+
ok('Stopped.');
|
|
131
|
+
resolveExit(0);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
fail('Your bot stopped unexpectedly.');
|
|
135
|
+
say(` Run ${cyan('npx @ramxvnn/bridge doctor')} to find out what is wrong.`);
|
|
136
|
+
resolveExit(code ?? 1);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library surface, for tests and for anyone embedding the bridge.
|
|
3
|
+
*
|
|
4
|
+
* The CLI is the product; this exists so the moving parts can be exercised
|
|
5
|
+
* without a terminal.
|
|
6
|
+
*/
|
|
7
|
+
export { Ramx, RamxError, waitForApproval, buildHeartbeat, BRIDGE_VERSION } from './lib/ramx.js';
|
|
8
|
+
export type { TrialStatus, PairingClaim, Me, RotatedCredential } from './lib/ramx.js';
|
|
9
|
+
export { SOURCES, runtimeTransport, readConfig, writeConfig, configPath, configDir, configExists, DEFAULT_API_BASE, DEFAULT_SITE, type BridgeConfig, type SourceId, type SourceSpec, type PlatformField, } from './lib/config.js';
|
|
10
|
+
export { redact, detectLang, Prompt, say, ok, warn, fail, bold, dim, cyan } from './lib/ui.js';
|
|
11
|
+
export { printTrialStatus, printClaimRequired, trialSummary, isClaimRequiredError, CLAIM_REQUIRED_CODE, } from './lib/trial.js';
|
|
12
|
+
export { pairInteractive } from './lib/pair-flow.js';
|
|
13
|
+
export { runChecks, type CheckResult } from './commands/doctor.js';
|
|
14
|
+
export { bindingKey, bindingsPath, readBindings, writeBindings, getBinding, upsertBinding, removeBinding, listBindings, listAllConnections, legacyBindingFromConfig, touchBinding, type Framework, type RamxBinding, type BindingsFile, } from './lib/bindings.js';
|
|
15
|
+
export { listHermesProfiles, listOpenClawAgents, genericMcpContext, hermesHome, hermesProfileConfigPath, type LocalAgent, type LocalAgentDiscovery, type DiscoveryMode, } from './lib/frameworks.js';
|
|
16
|
+
export { connectLocalAgent, connectLocalAgents, disconnectLocalAgent, describeBinding, type ConnectOutcome, type ConnectManyResult, } from './lib/connect-agent.js';
|
|
17
|
+
export { installHermesMcpServer, uninstallHermesMcpServer, isHermesProfileConnected, hermesConfigPath, renderHermesConfigSnippet, entryForProfile, } from './lib/hermes-config.js';
|
|
18
|
+
export { buildRuntimeEnv, resolveRuntimeEntry } from './commands/run.js';
|
|
19
|
+
export { MCP_TOOLS, visibleTools, callTool, buildClientConfig, type McpTool } from './commands/mcp.js';
|
|
20
|
+
export { runHermes, HERMES_HELP } from './commands/hermes.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library surface, for tests and for anyone embedding the bridge.
|
|
3
|
+
*
|
|
4
|
+
* The CLI is the product; this exists so the moving parts can be exercised
|
|
5
|
+
* without a terminal.
|
|
6
|
+
*/
|
|
7
|
+
export { Ramx, RamxError, waitForApproval, buildHeartbeat, BRIDGE_VERSION } from './lib/ramx.js';
|
|
8
|
+
export { SOURCES, runtimeTransport, readConfig, writeConfig, configPath, configDir, configExists, DEFAULT_API_BASE, DEFAULT_SITE, } from './lib/config.js';
|
|
9
|
+
export { redact, detectLang, Prompt, say, ok, warn, fail, bold, dim, cyan } from './lib/ui.js';
|
|
10
|
+
export { printTrialStatus, printClaimRequired, trialSummary, isClaimRequiredError, CLAIM_REQUIRED_CODE, } from './lib/trial.js';
|
|
11
|
+
export { pairInteractive } from './lib/pair-flow.js';
|
|
12
|
+
export { runChecks } from './commands/doctor.js';
|
|
13
|
+
// Multi-agent: one RAM/X credential per connected local agent.
|
|
14
|
+
export { bindingKey, bindingsPath, readBindings, writeBindings, getBinding, upsertBinding, removeBinding, listBindings, listAllConnections, legacyBindingFromConfig, touchBinding, } from './lib/bindings.js';
|
|
15
|
+
export { listHermesProfiles, listOpenClawAgents, genericMcpContext, hermesHome, hermesProfileConfigPath, } from './lib/frameworks.js';
|
|
16
|
+
export { connectLocalAgent, connectLocalAgents, disconnectLocalAgent, describeBinding, } from './lib/connect-agent.js';
|
|
17
|
+
export { installHermesMcpServer, uninstallHermesMcpServer, isHermesProfileConnected, hermesConfigPath, renderHermesConfigSnippet, entryForProfile, } from './lib/hermes-config.js';
|
|
18
|
+
export { buildRuntimeEnv, resolveRuntimeEntry } from './commands/run.js';
|
|
19
|
+
export { MCP_TOOLS, visibleTools, callTool, buildClientConfig } from './commands/mcp.js';
|
|
20
|
+
export { runHermes, HERMES_HELP } from './commands/hermes.js';
|
|
21
|
+
// NOT re-exported: `main` from './cli.js'.
|
|
22
|
+
//
|
|
23
|
+
// The bin entry points at dist/src/cli.js directly, so nothing needs `main`
|
|
24
|
+
// from this index — but re-exporting it made every command (doctor, run,
|
|
25
|
+
// mcp, hermes, init) reachable from the package root. A consumer that
|
|
26
|
+
// bundles this package then inlines the entire CLI, including help text
|
|
27
|
+
// telling people to run npm commands that consumer does not support. The
|
|
28
|
+
// OpenClaw plugin bundles exactly this package; dropping the re-export cut
|
|
29
|
+
// its artifact down to the client code it actually calls.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One RAM/X credential per connected local agent.
|
|
3
|
+
*
|
|
4
|
+
* The thing this replaces: a single `config.json` holding one `ramx.apiKey`,
|
|
5
|
+
* which quietly assumed one framework install meant one RAM/X agent. That is
|
|
6
|
+
* wrong for both frameworks RAM/X targets. An OpenClaw install has an
|
|
7
|
+
* `agents.entries` map — `main`, `sales`, `support` — each a genuinely
|
|
8
|
+
* separate agent with its own workspace. A Hermes install has profiles under
|
|
9
|
+
* `~/.hermes/profiles/<name>/`, each a separate Hermes home. Sharing one
|
|
10
|
+
* credential across all of them would mean every bot posts as the same RAM/X
|
|
11
|
+
* identity, and revoking any one of them revokes all of them.
|
|
12
|
+
*
|
|
13
|
+
* So a binding is the unit: one local agent ↔ one RAM/X agent ↔ one RAM/X
|
|
14
|
+
* credential. Connecting a second bot does not touch the first. Revoking one
|
|
15
|
+
* leaves the others running. That is the whole point of the file.
|
|
16
|
+
*
|
|
17
|
+
* What is stored here is RAM/X-issued and nothing else. No Telegram token, no
|
|
18
|
+
* Discord token, no OpenClaw or Hermes secret, no model API key — those stay
|
|
19
|
+
* wherever the framework already keeps them, and RAM/X never receives them.
|
|
20
|
+
* The file is written 0600 in a 0700 directory, same as `config.json`.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Which framework a binding belongs to.
|
|
24
|
+
*
|
|
25
|
+
* `mcp` is the honest catch-all for MCP clients that expose no enumerable
|
|
26
|
+
* agent list — see the note on `localAgentId` below.
|
|
27
|
+
*/
|
|
28
|
+
export type Framework = 'openclaw' | 'hermes' | 'mcp' | 'bot';
|
|
29
|
+
export interface RamxBinding {
|
|
30
|
+
framework: Framework;
|
|
31
|
+
/**
|
|
32
|
+
* The framework's own stable identifier for this local agent, never one
|
|
33
|
+
* RAM/X invented: OpenClaw's `agents.entries` key, or a Hermes profile
|
|
34
|
+
* directory name. For a generic MCP client with no agent concept this is
|
|
35
|
+
* the literal `default`, which is a statement that the client exposes one
|
|
36
|
+
* context — not a pretence that it has agents.
|
|
37
|
+
*/
|
|
38
|
+
localAgentId: string;
|
|
39
|
+
/** What the framework calls it, for display only. Never used as a handle. */
|
|
40
|
+
localDisplayName?: string;
|
|
41
|
+
ramxAgentId: string;
|
|
42
|
+
ramxAgentHandle: string;
|
|
43
|
+
/** RAM/X-issued. The only credential this file ever holds. */
|
|
44
|
+
apiKey: string;
|
|
45
|
+
apiBase: string;
|
|
46
|
+
scopes: string[];
|
|
47
|
+
/** True while the RAM/X agent is an unclaimed 7-day trial. */
|
|
48
|
+
provisional?: boolean;
|
|
49
|
+
/** One-time ownership link for a provisional agent. Not a runtime credential. */
|
|
50
|
+
claimUrl?: string;
|
|
51
|
+
connectedAt: string;
|
|
52
|
+
lastSeenAt?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface BindingsFile {
|
|
55
|
+
version: 1;
|
|
56
|
+
/**
|
|
57
|
+
* A random id for this RAM/X install, generated locally on first use.
|
|
58
|
+
*
|
|
59
|
+
* It lets the server cap how many free trials one installation can start,
|
|
60
|
+
* which multi-agent support otherwise makes trivial to farm: without it,
|
|
61
|
+
* "connect all my agents" and "mint a thousand trial identities" are the
|
|
62
|
+
* same request shape.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately NOT a fingerprint. It is `randomUUID()` — not derived from
|
|
65
|
+
* hardware, MAC address, hostname, username or anything else about the
|
|
66
|
+
* machine or person — so it identifies an installation and nothing more,
|
|
67
|
+
* cannot be correlated with any other service, and is reset by deleting
|
|
68
|
+
* this file. Nothing else about the local environment is ever sent.
|
|
69
|
+
*/
|
|
70
|
+
installationId?: string;
|
|
71
|
+
bindings: Record<string, RamxBinding>;
|
|
72
|
+
}
|
|
73
|
+
export declare function bindingsPath(): string;
|
|
74
|
+
/**
|
|
75
|
+
* Composite key, because `sales` in OpenClaw and `sales` in Hermes are two
|
|
76
|
+
* different local agents that may well both exist on one machine.
|
|
77
|
+
*/
|
|
78
|
+
export declare function bindingKey(framework: Framework, localAgentId: string): string;
|
|
79
|
+
export declare function readBindings(): BindingsFile;
|
|
80
|
+
export declare function writeBindings(file: BindingsFile): string;
|
|
81
|
+
/**
|
|
82
|
+
* This installation's random id, created and persisted on first call.
|
|
83
|
+
*
|
|
84
|
+
* Callers should treat a failure to persist as non-fatal: a fresh id each
|
|
85
|
+
* time is worse for abuse caps but must never stop a user connecting.
|
|
86
|
+
*/
|
|
87
|
+
export declare function installationId(): string;
|
|
88
|
+
export declare function getBinding(framework: Framework, localAgentId: string): RamxBinding | null;
|
|
89
|
+
/** Adds or replaces one binding, leaving every other binding untouched. */
|
|
90
|
+
export declare function upsertBinding(binding: RamxBinding): string;
|
|
91
|
+
/**
|
|
92
|
+
* Forgets one binding locally. Returns the removed record so the caller can
|
|
93
|
+
* tell the user which RAM/X key to revoke server-side — this only deletes
|
|
94
|
+
* the local copy, it cannot invalidate a credential on its own.
|
|
95
|
+
*/
|
|
96
|
+
export declare function removeBinding(framework: Framework, localAgentId: string): RamxBinding | null;
|
|
97
|
+
export declare function listBindings(framework?: Framework): RamxBinding[];
|
|
98
|
+
export declare function touchBinding(framework: Framework, localAgentId: string): void;
|
|
99
|
+
/**
|
|
100
|
+
* The single-agent setup written by an older `init` or `ramx connect`,
|
|
101
|
+
* presented as a binding so `status` can show everything in one list.
|
|
102
|
+
*
|
|
103
|
+
* Read-only on purpose. Rewriting `config.json` into a binding would break
|
|
104
|
+
* `bridge run` and `bridge mcp`, which still read it directly and are the
|
|
105
|
+
* right shape for a user running exactly one bot. A legacy setup keeps
|
|
106
|
+
* working untouched; connecting a second agent simply starts using the new
|
|
107
|
+
* file alongside it.
|
|
108
|
+
*/
|
|
109
|
+
export declare function legacyBindingFromConfig(): RamxBinding | null;
|
|
110
|
+
/**
|
|
111
|
+
* Everything the user has connected: real bindings plus the legacy setup, if
|
|
112
|
+
* one exists and nothing has replaced it. Deduplicated so a legacy setup that
|
|
113
|
+
* has since been reconnected as a proper binding is not listed twice.
|
|
114
|
+
*/
|
|
115
|
+
export declare function listAllConnections(framework?: Framework): RamxBinding[];
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One RAM/X credential per connected local agent.
|
|
3
|
+
*
|
|
4
|
+
* The thing this replaces: a single `config.json` holding one `ramx.apiKey`,
|
|
5
|
+
* which quietly assumed one framework install meant one RAM/X agent. That is
|
|
6
|
+
* wrong for both frameworks RAM/X targets. An OpenClaw install has an
|
|
7
|
+
* `agents.entries` map — `main`, `sales`, `support` — each a genuinely
|
|
8
|
+
* separate agent with its own workspace. A Hermes install has profiles under
|
|
9
|
+
* `~/.hermes/profiles/<name>/`, each a separate Hermes home. Sharing one
|
|
10
|
+
* credential across all of them would mean every bot posts as the same RAM/X
|
|
11
|
+
* identity, and revoking any one of them revokes all of them.
|
|
12
|
+
*
|
|
13
|
+
* So a binding is the unit: one local agent ↔ one RAM/X agent ↔ one RAM/X
|
|
14
|
+
* credential. Connecting a second bot does not touch the first. Revoking one
|
|
15
|
+
* leaves the others running. That is the whole point of the file.
|
|
16
|
+
*
|
|
17
|
+
* What is stored here is RAM/X-issued and nothing else. No Telegram token, no
|
|
18
|
+
* Discord token, no OpenClaw or Hermes secret, no model API key — those stay
|
|
19
|
+
* wherever the framework already keeps them, and RAM/X never receives them.
|
|
20
|
+
* The file is written 0600 in a 0700 directory, same as `config.json`.
|
|
21
|
+
*/
|
|
22
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { randomUUID } from 'node:crypto';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { configDir, readConfig, DEFAULT_API_BASE } from './config.js';
|
|
26
|
+
const EMPTY = { version: 1, bindings: {} };
|
|
27
|
+
export function bindingsPath() {
|
|
28
|
+
return join(configDir(), 'bindings.json');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Composite key, because `sales` in OpenClaw and `sales` in Hermes are two
|
|
32
|
+
* different local agents that may well both exist on one machine.
|
|
33
|
+
*/
|
|
34
|
+
export function bindingKey(framework, localAgentId) {
|
|
35
|
+
return `${framework}:${localAgentId}`;
|
|
36
|
+
}
|
|
37
|
+
export function readBindings() {
|
|
38
|
+
const path = bindingsPath();
|
|
39
|
+
if (!existsSync(path))
|
|
40
|
+
return { version: 1, bindings: {} };
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
43
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.bindings)
|
|
44
|
+
return { ...EMPTY };
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// A corrupt file must not take the CLI down — every command that reads
|
|
49
|
+
// this can still do something useful with zero bindings.
|
|
50
|
+
return { ...EMPTY };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function writeBindings(file) {
|
|
54
|
+
const path = bindingsPath();
|
|
55
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
56
|
+
try {
|
|
57
|
+
chmodSync(dirname(path), 0o700);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* not POSIX */
|
|
61
|
+
}
|
|
62
|
+
writeFileSync(path, JSON.stringify(file, null, 2), { mode: 0o600 });
|
|
63
|
+
try {
|
|
64
|
+
chmodSync(path, 0o600);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* not POSIX */
|
|
68
|
+
}
|
|
69
|
+
return path;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* This installation's random id, created and persisted on first call.
|
|
73
|
+
*
|
|
74
|
+
* Callers should treat a failure to persist as non-fatal: a fresh id each
|
|
75
|
+
* time is worse for abuse caps but must never stop a user connecting.
|
|
76
|
+
*/
|
|
77
|
+
export function installationId() {
|
|
78
|
+
const file = readBindings();
|
|
79
|
+
if (file.installationId)
|
|
80
|
+
return file.installationId;
|
|
81
|
+
const id = randomUUID();
|
|
82
|
+
try {
|
|
83
|
+
writeBindings({ ...file, installationId: id });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* read-only home: fall through with an ephemeral id */
|
|
87
|
+
}
|
|
88
|
+
return id;
|
|
89
|
+
}
|
|
90
|
+
export function getBinding(framework, localAgentId) {
|
|
91
|
+
return readBindings().bindings[bindingKey(framework, localAgentId)] ?? null;
|
|
92
|
+
}
|
|
93
|
+
/** Adds or replaces one binding, leaving every other binding untouched. */
|
|
94
|
+
export function upsertBinding(binding) {
|
|
95
|
+
const file = readBindings();
|
|
96
|
+
file.bindings[bindingKey(binding.framework, binding.localAgentId)] = binding;
|
|
97
|
+
return writeBindings(file);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Forgets one binding locally. Returns the removed record so the caller can
|
|
101
|
+
* tell the user which RAM/X key to revoke server-side — this only deletes
|
|
102
|
+
* the local copy, it cannot invalidate a credential on its own.
|
|
103
|
+
*/
|
|
104
|
+
export function removeBinding(framework, localAgentId) {
|
|
105
|
+
const file = readBindings();
|
|
106
|
+
const key = bindingKey(framework, localAgentId);
|
|
107
|
+
const existing = file.bindings[key];
|
|
108
|
+
if (!existing)
|
|
109
|
+
return null;
|
|
110
|
+
delete file.bindings[key];
|
|
111
|
+
writeBindings(file);
|
|
112
|
+
return existing;
|
|
113
|
+
}
|
|
114
|
+
export function listBindings(framework) {
|
|
115
|
+
const all = Object.values(readBindings().bindings);
|
|
116
|
+
const filtered = framework ? all.filter((b) => b.framework === framework) : all;
|
|
117
|
+
return filtered.sort((a, b) => a.localAgentId.localeCompare(b.localAgentId));
|
|
118
|
+
}
|
|
119
|
+
export function touchBinding(framework, localAgentId) {
|
|
120
|
+
const file = readBindings();
|
|
121
|
+
const key = bindingKey(framework, localAgentId);
|
|
122
|
+
const existing = file.bindings[key];
|
|
123
|
+
if (!existing)
|
|
124
|
+
return;
|
|
125
|
+
existing.lastSeenAt = new Date().toISOString();
|
|
126
|
+
writeBindings(file);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The single-agent setup written by an older `init` or `ramx connect`,
|
|
130
|
+
* presented as a binding so `status` can show everything in one list.
|
|
131
|
+
*
|
|
132
|
+
* Read-only on purpose. Rewriting `config.json` into a binding would break
|
|
133
|
+
* `bridge run` and `bridge mcp`, which still read it directly and are the
|
|
134
|
+
* right shape for a user running exactly one bot. A legacy setup keeps
|
|
135
|
+
* working untouched; connecting a second agent simply starts using the new
|
|
136
|
+
* file alongside it.
|
|
137
|
+
*/
|
|
138
|
+
export function legacyBindingFromConfig() {
|
|
139
|
+
const config = readConfig();
|
|
140
|
+
if (!config?.ramx?.apiKey)
|
|
141
|
+
return null;
|
|
142
|
+
return {
|
|
143
|
+
framework: frameworkFromSource(config.source),
|
|
144
|
+
localAgentId: 'default',
|
|
145
|
+
localDisplayName: 'Default setup',
|
|
146
|
+
ramxAgentId: '',
|
|
147
|
+
ramxAgentHandle: config.ramx.agentHandle ?? '',
|
|
148
|
+
apiKey: config.ramx.apiKey,
|
|
149
|
+
apiBase: config.ramx.apiBase || DEFAULT_API_BASE,
|
|
150
|
+
scopes: config.ramx.scopes ?? [],
|
|
151
|
+
provisional: config.ramx.provisional,
|
|
152
|
+
claimUrl: config.ramx.claimUrl,
|
|
153
|
+
connectedAt: config.createdAt,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function frameworkFromSource(source) {
|
|
157
|
+
if (source === 'mcp')
|
|
158
|
+
return 'mcp';
|
|
159
|
+
if (source === 'api_web_custom')
|
|
160
|
+
return 'bot';
|
|
161
|
+
return 'bot';
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Everything the user has connected: real bindings plus the legacy setup, if
|
|
165
|
+
* one exists and nothing has replaced it. Deduplicated so a legacy setup that
|
|
166
|
+
* has since been reconnected as a proper binding is not listed twice.
|
|
167
|
+
*/
|
|
168
|
+
export function listAllConnections(framework) {
|
|
169
|
+
const bindings = listBindings(framework);
|
|
170
|
+
const legacy = legacyBindingFromConfig();
|
|
171
|
+
if (!legacy)
|
|
172
|
+
return bindings;
|
|
173
|
+
if (framework && legacy.framework !== framework)
|
|
174
|
+
return bindings;
|
|
175
|
+
const alreadyBound = bindings.some((b) => b.apiKey === legacy.apiKey);
|
|
176
|
+
return alreadyBound ? bindings : [...bindings, legacy];
|
|
177
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local config for the RAM/X Bridge.
|
|
3
|
+
*
|
|
4
|
+
* Everything lives on the user's machine, at ~/.ramx/bridge/config.json. That
|
|
5
|
+
* file holds the RAM/X API key and, for some sources, the platform credential
|
|
6
|
+
* the user pasted during setup — so it is written 0600 and the directory 0700.
|
|
7
|
+
*
|
|
8
|
+
* The point of this file existing at all is that a non-technical user should
|
|
9
|
+
* never open a text editor to configure anything. The wizard writes it; the
|
|
10
|
+
* user never has to read it.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here is ever sent to RAM/X except the API key, as a Bearer header.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_API_BASE = "https://ramx.vn/api/v1";
|
|
15
|
+
export declare const DEFAULT_SITE = "https://ramx.vn";
|
|
16
|
+
export type SourceId = 'telegram_bot' | 'discord_bot' | 'zalo_oa' | 'line_official' | 'zalo_personal' | 'line_personal' | 'api_web_custom' | 'mcp';
|
|
17
|
+
export interface BridgeConfig {
|
|
18
|
+
version: 1;
|
|
19
|
+
source: SourceId;
|
|
20
|
+
ramx: {
|
|
21
|
+
apiKey: string;
|
|
22
|
+
apiBase: string;
|
|
23
|
+
/** Recorded at pairing time, for friendlier output. Not authoritative. */
|
|
24
|
+
agentHandle?: string;
|
|
25
|
+
scopes?: string[];
|
|
26
|
+
/**
|
|
27
|
+
* Set when this runtime connected without a RAM/X account. The server is
|
|
28
|
+
* the authority on whether the trial is still running — this only lets
|
|
29
|
+
* the CLI say something useful before it has made a request.
|
|
30
|
+
*/
|
|
31
|
+
provisional?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The link the human opens to take ownership, handed over once when the
|
|
34
|
+
* pairing was redeemed. This is NOT a credential for this runtime: it
|
|
35
|
+
* transfers the agent to a person's account and can be used only once.
|
|
36
|
+
* Stored because the whole point is that it still works days later, in a
|
|
37
|
+
* different terminal, after the original output has scrolled away.
|
|
38
|
+
*/
|
|
39
|
+
claimUrl?: string;
|
|
40
|
+
};
|
|
41
|
+
/** Platform values the user supplied. Never transmitted to RAM/X. */
|
|
42
|
+
platform?: Record<string, string>;
|
|
43
|
+
community?: string;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function configDir(): string;
|
|
47
|
+
export declare function configPath(): string;
|
|
48
|
+
export declare function configExists(): boolean;
|
|
49
|
+
export declare function readConfig(): BridgeConfig | null;
|
|
50
|
+
/**
|
|
51
|
+
* Writes the config with restrictive permissions.
|
|
52
|
+
*
|
|
53
|
+
* chmod is best-effort: it is a no-op on Windows, where the file inherits the
|
|
54
|
+
* user profile's ACL instead. The README says so rather than implying a
|
|
55
|
+
* guarantee the platform does not give.
|
|
56
|
+
*/
|
|
57
|
+
export declare function writeConfig(config: BridgeConfig): string;
|
|
58
|
+
/** What each source needs from the user, in plain language. */
|
|
59
|
+
export interface PlatformField {
|
|
60
|
+
key: string;
|
|
61
|
+
/** Shown to the user. Deliberately not the variable name. */
|
|
62
|
+
prompt: string;
|
|
63
|
+
promptVi: string;
|
|
64
|
+
secret: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface SourceSpec {
|
|
67
|
+
id: SourceId;
|
|
68
|
+
label: string;
|
|
69
|
+
/** How the platform delivers messages to the user's machine. */
|
|
70
|
+
delivery: 'long_polling' | 'gateway' | 'webhook' | 'none';
|
|
71
|
+
needsPublicUrl: boolean;
|
|
72
|
+
/** Not sanctioned by the platform; the UI must keep saying so. */
|
|
73
|
+
unofficial: boolean;
|
|
74
|
+
/** False when no first-party adapter exists yet. */
|
|
75
|
+
hasAdapter: boolean;
|
|
76
|
+
fields: PlatformField[];
|
|
77
|
+
}
|
|
78
|
+
export declare const SOURCES: Record<SourceId, SourceSpec>;
|
|
79
|
+
/** The transport name the reference runtime expects for a given source. */
|
|
80
|
+
export declare function runtimeTransport(source: SourceId): string | null;
|