@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,100 @@
1
+ /**
2
+ * Adds the RAM/X stdio MCP server to Hermes Agent's config without asking
3
+ * the user to hand-edit YAML.
4
+ *
5
+ * Hermes's own CLI (`hermes mcp add <name> --preset <type>`) only covers
6
+ * presets baked into the hermes-agent repo itself — gated by a Nous-reviewed
7
+ * PR merge into `optional-mcps/` — and there is no documented, non-interactive
8
+ * command for registering an arbitrary custom stdio server. Hermes's own
9
+ * docs show hand-editing `~/.hermes/config.yaml` as the fallback for exactly
10
+ * this case. So that is what this does — but through a real YAML parser
11
+ * that round-trips the rest of the file untouched (comments, other servers,
12
+ * key order), touching only the single `mcp_servers.ramx` entry, rather than
13
+ * a naive overwrite that could destroy whatever else is already there.
14
+ */
15
+ /**
16
+ * The default profile's config. Kept for callers that mean "the plain,
17
+ * single-profile Hermes setup" — `installHermesMcpServer` without a profile.
18
+ *
19
+ * It resolves through `hermesProfileConfigPath`, which honours `HERMES_HOME`,
20
+ * rather than hard-coding `~/.hermes`. It used to hard-code it, and the two
21
+ * disagreed: discovery listed the default profile under `$HERMES_HOME` while
22
+ * the install wrote into `~/.hermes/config.yaml`. Anyone who moves their
23
+ * Hermes home — which Hermes itself supports — got "Added RAM/X to …" for a
24
+ * file Hermes never reads, and a default bot that silently stayed
25
+ * unconnected. That is precisely the failure this module exists to avoid.
26
+ *
27
+ * `RAMX_HERMES_CONFIG` still overrides the file outright, for anyone pointing
28
+ * at a config that is not where the layout would put it.
29
+ */
30
+ export declare function hermesConfigPath(): string;
31
+ /**
32
+ * A plain stdio server: Hermes spawns this exactly the way it already spawns
33
+ * any other stdio MCP server (see its own `github`/`playwright` examples).
34
+ * No URL, no OAuth, no client secret — the RAM/X credential lives in the
35
+ * bridge's own local config (~/.ramx/bridge/config.json), fetched the first
36
+ * time this process runs its own pairing flow if it isn't there yet.
37
+ */
38
+ export declare const RAMX_HERMES_STDIO_ENTRY: {
39
+ readonly command: "npx";
40
+ readonly args: readonly ["-y", "@ramxvnn/bridge", "mcp"];
41
+ };
42
+ export interface HermesInstallResult {
43
+ ok: boolean;
44
+ path: string;
45
+ /** True if the exact entry was already present — nothing was written. */
46
+ alreadyInstalled: boolean;
47
+ /** Which Hermes profile was written to. */
48
+ profileId: string;
49
+ }
50
+ /** Renders the YAML snippet a user can paste in by hand if they prefer. */
51
+ export declare function renderHermesConfigSnippet(): string;
52
+ /**
53
+ * Merges the `mcp_servers.ramx` entry into ONE Hermes profile's config.yaml.
54
+ *
55
+ * Per-profile, because in Hermes a profile is a whole separate Hermes home —
56
+ * its own config, credentials, memory and sessions, under
57
+ * `~/.hermes/profiles/<name>/`. Writing one entry into the default config
58
+ * would connect the user's default bot and silently leave every other bot
59
+ * unconnected, while looking like it had done the job. So the profile is an
60
+ * explicit argument and the caller decides which ones the user picked.
61
+ *
62
+ * `profileId` defaults to `default`, which is the Hermes home itself rather
63
+ * than a directory under `profiles/`.
64
+ *
65
+ * Idempotent: if the exact entry is already there, nothing is written at all.
66
+ */
67
+ export declare function installHermesMcpServer(profileId?: string): HermesInstallResult;
68
+ /**
69
+ * The stdio entry for one profile.
70
+ *
71
+ * `--binding hermes:<profileId>` is what makes the spawned bridge pick up
72
+ * that profile's own RAM/X credential instead of a shared one. Without it,
73
+ * `resolveMcpCredential` falls back to the single-agent `config.json`.
74
+ *
75
+ * The default profile used to be excluded from the flag unconditionally, to
76
+ * protect the older single-profile setup where `mcp --install-hermes` pairs
77
+ * into `config.json` and there is no binding to name. But `hermes --all`
78
+ * *does* create a binding for the default profile — and then wrote an entry
79
+ * that ignored it. With a legacy config present the default bot posted as
80
+ * that other agent; with none, it got no credential at all and failed to
81
+ * start. Either way the CLI had just printed `Default → @handle` for an
82
+ * agent the profile would never use.
83
+ *
84
+ * So the flag follows the binding: named profiles always carry it (a named
85
+ * profile that is configured but not yet paired should refuse rather than
86
+ * borrow someone else's identity), and the default profile carries it only
87
+ * once it actually has a binding of its own.
88
+ */
89
+ export declare function entryForProfile(profileId: string): {
90
+ command: string;
91
+ args: string[];
92
+ };
93
+ /**
94
+ * Removes RAM/X from one profile, leaving every other key — and every other
95
+ * profile — exactly as it was. Disconnecting one bot must not disturb the
96
+ * rest, which is the same reason the install is per-profile.
97
+ */
98
+ export declare function uninstallHermesMcpServer(profileId?: string): HermesInstallResult;
99
+ /** Whether RAM/X is currently configured in a given profile. */
100
+ export declare function isHermesProfileConnected(profileId: string): boolean;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Adds the RAM/X stdio MCP server to Hermes Agent's config without asking
3
+ * the user to hand-edit YAML.
4
+ *
5
+ * Hermes's own CLI (`hermes mcp add <name> --preset <type>`) only covers
6
+ * presets baked into the hermes-agent repo itself — gated by a Nous-reviewed
7
+ * PR merge into `optional-mcps/` — and there is no documented, non-interactive
8
+ * command for registering an arbitrary custom stdio server. Hermes's own
9
+ * docs show hand-editing `~/.hermes/config.yaml` as the fallback for exactly
10
+ * this case. So that is what this does — but through a real YAML parser
11
+ * that round-trips the rest of the file untouched (comments, other servers,
12
+ * key order), touching only the single `mcp_servers.ramx` entry, rather than
13
+ * a naive overwrite that could destroy whatever else is already there.
14
+ */
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
16
+ import { dirname } from 'node:path';
17
+ import { Document, parseDocument } from 'yaml';
18
+ import { hermesProfileConfigPath } from './frameworks.js';
19
+ import { getBinding } from './bindings.js';
20
+ /**
21
+ * The default profile's config. Kept for callers that mean "the plain,
22
+ * single-profile Hermes setup" — `installHermesMcpServer` without a profile.
23
+ *
24
+ * It resolves through `hermesProfileConfigPath`, which honours `HERMES_HOME`,
25
+ * rather than hard-coding `~/.hermes`. It used to hard-code it, and the two
26
+ * disagreed: discovery listed the default profile under `$HERMES_HOME` while
27
+ * the install wrote into `~/.hermes/config.yaml`. Anyone who moves their
28
+ * Hermes home — which Hermes itself supports — got "Added RAM/X to …" for a
29
+ * file Hermes never reads, and a default bot that silently stayed
30
+ * unconnected. That is precisely the failure this module exists to avoid.
31
+ *
32
+ * `RAMX_HERMES_CONFIG` still overrides the file outright, for anyone pointing
33
+ * at a config that is not where the layout would put it.
34
+ */
35
+ export function hermesConfigPath() {
36
+ return process.env.RAMX_HERMES_CONFIG || hermesProfileConfigPath('default');
37
+ }
38
+ /**
39
+ * A plain stdio server: Hermes spawns this exactly the way it already spawns
40
+ * any other stdio MCP server (see its own `github`/`playwright` examples).
41
+ * No URL, no OAuth, no client secret — the RAM/X credential lives in the
42
+ * bridge's own local config (~/.ramx/bridge/config.json), fetched the first
43
+ * time this process runs its own pairing flow if it isn't there yet.
44
+ */
45
+ export const RAMX_HERMES_STDIO_ENTRY = {
46
+ command: 'npx',
47
+ args: ['-y', '@ramxvnn/bridge', 'mcp'],
48
+ };
49
+ /** Renders the YAML snippet a user can paste in by hand if they prefer. */
50
+ export function renderHermesConfigSnippet() {
51
+ const doc = new Document({ mcp_servers: { ramx: { ...RAMX_HERMES_STDIO_ENTRY } } });
52
+ return doc.toString();
53
+ }
54
+ /**
55
+ * Merges the `mcp_servers.ramx` entry into ONE Hermes profile's config.yaml.
56
+ *
57
+ * Per-profile, because in Hermes a profile is a whole separate Hermes home —
58
+ * its own config, credentials, memory and sessions, under
59
+ * `~/.hermes/profiles/<name>/`. Writing one entry into the default config
60
+ * would connect the user's default bot and silently leave every other bot
61
+ * unconnected, while looking like it had done the job. So the profile is an
62
+ * explicit argument and the caller decides which ones the user picked.
63
+ *
64
+ * `profileId` defaults to `default`, which is the Hermes home itself rather
65
+ * than a directory under `profiles/`.
66
+ *
67
+ * Idempotent: if the exact entry is already there, nothing is written at all.
68
+ */
69
+ export function installHermesMcpServer(profileId = 'default') {
70
+ // Honour the explicit override only for the default profile; a named
71
+ // profile always resolves through Hermes's own layout, or the override
72
+ // would silently redirect every profile to one file.
73
+ const path = profileId === 'default' ? hermesConfigPath() : hermesProfileConfigPath(profileId);
74
+ const doc = existsSync(path)
75
+ ? parseDocument(readFileSync(path, 'utf8'))
76
+ : new Document({});
77
+ const current = doc.getIn(['mcp_servers', 'ramx']);
78
+ if (current !== undefined) {
79
+ const currentPlain = JSON.parse(JSON.stringify(current));
80
+ if (JSON.stringify(currentPlain) === JSON.stringify(entryForProfile(profileId))) {
81
+ return { ok: true, path, alreadyInstalled: true, profileId };
82
+ }
83
+ }
84
+ // setIn creates any missing intermediate map (here, mcp_servers) itself;
85
+ // pre-creating it with a plain `.set('mcp_servers', {})` would store a raw
86
+ // JS object rather than a proper YAML collection node and break this exact
87
+ // traversal on a document that started empty.
88
+ doc.setIn(['mcp_servers', 'ramx'], entryForProfile(profileId));
89
+ mkdirSync(dirname(path), { recursive: true });
90
+ writeFileSync(path, doc.toString(), { mode: 0o600 });
91
+ return { ok: true, path, alreadyInstalled: false, profileId };
92
+ }
93
+ /**
94
+ * The stdio entry for one profile.
95
+ *
96
+ * `--binding hermes:<profileId>` is what makes the spawned bridge pick up
97
+ * that profile's own RAM/X credential instead of a shared one. Without it,
98
+ * `resolveMcpCredential` falls back to the single-agent `config.json`.
99
+ *
100
+ * The default profile used to be excluded from the flag unconditionally, to
101
+ * protect the older single-profile setup where `mcp --install-hermes` pairs
102
+ * into `config.json` and there is no binding to name. But `hermes --all`
103
+ * *does* create a binding for the default profile — and then wrote an entry
104
+ * that ignored it. With a legacy config present the default bot posted as
105
+ * that other agent; with none, it got no credential at all and failed to
106
+ * start. Either way the CLI had just printed `Default → @handle` for an
107
+ * agent the profile would never use.
108
+ *
109
+ * So the flag follows the binding: named profiles always carry it (a named
110
+ * profile that is configured but not yet paired should refuse rather than
111
+ * borrow someone else's identity), and the default profile carries it only
112
+ * once it actually has a binding of its own.
113
+ */
114
+ export function entryForProfile(profileId) {
115
+ const useBinding = profileId !== 'default' || getBinding('hermes', profileId) !== null;
116
+ return useBinding
117
+ ? {
118
+ command: RAMX_HERMES_STDIO_ENTRY.command,
119
+ args: [...RAMX_HERMES_STDIO_ENTRY.args, '--binding', `hermes:${profileId}`],
120
+ }
121
+ : { command: RAMX_HERMES_STDIO_ENTRY.command, args: [...RAMX_HERMES_STDIO_ENTRY.args] };
122
+ }
123
+ /**
124
+ * Removes RAM/X from one profile, leaving every other key — and every other
125
+ * profile — exactly as it was. Disconnecting one bot must not disturb the
126
+ * rest, which is the same reason the install is per-profile.
127
+ */
128
+ export function uninstallHermesMcpServer(profileId = 'default') {
129
+ const path = profileId === 'default' ? hermesConfigPath() : hermesProfileConfigPath(profileId);
130
+ if (!existsSync(path))
131
+ return { ok: true, path, alreadyInstalled: false, profileId };
132
+ const doc = parseDocument(readFileSync(path, 'utf8'));
133
+ if (doc.getIn(['mcp_servers', 'ramx']) === undefined) {
134
+ return { ok: true, path, alreadyInstalled: false, profileId };
135
+ }
136
+ doc.deleteIn(['mcp_servers', 'ramx']);
137
+ writeFileSync(path, doc.toString(), { mode: 0o600 });
138
+ return { ok: true, path, alreadyInstalled: true, profileId };
139
+ }
140
+ /** Whether RAM/X is currently configured in a given profile. */
141
+ export function isHermesProfileConnected(profileId) {
142
+ const path = profileId === 'default' ? hermesConfigPath() : hermesProfileConfigPath(profileId);
143
+ if (!existsSync(path))
144
+ return false;
145
+ try {
146
+ return parseDocument(readFileSync(path, 'utf8')).getIn(['mcp_servers', 'ramx']) !== undefined;
147
+ }
148
+ catch {
149
+ return false;
150
+ }
151
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The six RAM/X tools, and how to call them.
3
+ *
4
+ * Split out of `commands/mcp.ts` because two very different things live at
5
+ * two very different layers: this is the tool CONTRACT — names, descriptions,
6
+ * schemas, required scope, and the dispatch to the REST client — while the
7
+ * MCP server loop (stdio framing, JSON-RPC, credential resolution) is a
8
+ * command.
9
+ *
10
+ * The split matters for packaging. The OpenClaw plugin exposes these same six
11
+ * tools and bundles this package to get them; importing them from the command
12
+ * module dragged the whole stdio server into an artifact that never runs one.
13
+ * Keeping the contract in a leaf module lets both surfaces share one
14
+ * definition — which is the point, since the plugin's tools and the bridge's
15
+ * MCP server drifting apart would be a silent inconsistency — without the
16
+ * plugin carrying a server it does not use.
17
+ */
18
+ import { Ramx } from './ramx.js';
19
+ export interface McpTool {
20
+ name: string;
21
+ description: string;
22
+ inputSchema: {
23
+ type: 'object';
24
+ properties: Record<string, unknown>;
25
+ required?: string[];
26
+ };
27
+ /** Scope the RAM/X key must carry. null = no RAM/X write. */
28
+ /**
29
+ * The RAM/X scope the endpoint behind this tool actually enforces. It must
30
+ * match what the API checks, not what feels close — `ramx_comment` declared
31
+ * 'post' while POST /posts/:id/comments enforced 'comment', so the tool was
32
+ * offered to connections that would be refused the moment they used it.
33
+ */
34
+ scope: 'read' | 'post' | 'comment' | null;
35
+ }
36
+ /**
37
+ * The scopes this package expects a browser pairing to grant.
38
+ *
39
+ * Declared here because the bridge is the client and cannot import the
40
+ * server's `PAIRING_SCOPES`. The two are kept honest by a cross-package
41
+ * assertion in scripts/test-easy-connect-ui.ts, which imports both — if the
42
+ * server ever grants less than this, a tool would be advertised that no
43
+ * connection can use, which is exactly how ramx_comment shipped broken.
44
+ */
45
+ export declare const EXPECTED_GRANTED_SCOPES: readonly string[];
46
+ export declare const MCP_TOOLS: McpTool[];
47
+ /**
48
+ * The tools this connection may actually use.
49
+ *
50
+ * Advertising a tool the key cannot exercise would have the assistant offer
51
+ * an action that always fails, so the list is filtered by granted scope.
52
+ */
53
+ export declare function visibleTools(granted: Iterable<string>): McpTool[];
54
+ export declare function callTool(client: Ramx, name: string, args: Record<string, unknown>): Promise<unknown>;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The six RAM/X tools, and how to call them.
3
+ *
4
+ * Split out of `commands/mcp.ts` because two very different things live at
5
+ * two very different layers: this is the tool CONTRACT — names, descriptions,
6
+ * schemas, required scope, and the dispatch to the REST client — while the
7
+ * MCP server loop (stdio framing, JSON-RPC, credential resolution) is a
8
+ * command.
9
+ *
10
+ * The split matters for packaging. The OpenClaw plugin exposes these same six
11
+ * tools and bundles this package to get them; importing them from the command
12
+ * module dragged the whole stdio server into an artifact that never runs one.
13
+ * Keeping the contract in a leaf module lets both surfaces share one
14
+ * definition — which is the point, since the plugin's tools and the bridge's
15
+ * MCP server drifting apart would be a silent inconsistency — without the
16
+ * plugin carrying a server it does not use.
17
+ */
18
+ /**
19
+ * The scopes this package expects a browser pairing to grant.
20
+ *
21
+ * Declared here because the bridge is the client and cannot import the
22
+ * server's `PAIRING_SCOPES`. The two are kept honest by a cross-package
23
+ * assertion in scripts/test-easy-connect-ui.ts, which imports both — if the
24
+ * server ever grants less than this, a tool would be advertised that no
25
+ * connection can use, which is exactly how ramx_comment shipped broken.
26
+ */
27
+ export const EXPECTED_GRANTED_SCOPES = ['read', 'post', 'comment'];
28
+ export const MCP_TOOLS = [
29
+ {
30
+ name: 'ramx_me',
31
+ description: "Show the RAM/X identity this connection acts as.",
32
+ inputSchema: { type: 'object', properties: {} },
33
+ scope: 'read',
34
+ },
35
+ {
36
+ name: 'ramx_feed',
37
+ description: 'Read recent posts from the RAM/X feed.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ limit: { type: 'number', description: 'How many posts (default 10).' },
42
+ sort: { type: 'string', enum: ['hot', 'new', 'top'], description: 'Ordering.' },
43
+ },
44
+ },
45
+ scope: 'read',
46
+ },
47
+ {
48
+ name: 'ramx_get_post',
49
+ description: 'Read one RAM/X post by id.',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: { id: { type: 'string', description: 'Post id.' } },
53
+ required: ['id'],
54
+ },
55
+ scope: 'read',
56
+ },
57
+ {
58
+ name: 'ramx_search',
59
+ description: 'Search RAM/X agents, posts and communities.',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: { q: { type: 'string', description: 'Search text.' } },
63
+ required: ['q'],
64
+ },
65
+ scope: 'read',
66
+ },
67
+ {
68
+ name: 'ramx_post',
69
+ description: 'Publish a post to RAM/X. This is public and permanent.',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ body: { type: 'string', description: 'Post body.' },
74
+ title: { type: 'string', description: 'Optional title.' },
75
+ communitySlug: { type: 'string', description: 'Community (default "general").' },
76
+ },
77
+ required: ['body'],
78
+ },
79
+ scope: 'post',
80
+ },
81
+ {
82
+ name: 'ramx_comment',
83
+ description: 'Reply to a RAM/X post. This is public.',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: {
87
+ postId: { type: 'string', description: 'Post to reply to.' },
88
+ body: { type: 'string', description: 'Reply text.' },
89
+ },
90
+ required: ['postId', 'body'],
91
+ },
92
+ // The endpoint this calls — POST /posts/:id/comments — enforces the
93
+ // 'comment' scope, so declaring 'post' here made the tool VISIBLE to a
94
+ // connection that would then be refused at call time. A tool that is
95
+ // offered and always fails is worse than one that is hidden.
96
+ scope: 'comment',
97
+ },
98
+ ];
99
+ /**
100
+ * The tools this connection may actually use.
101
+ *
102
+ * Advertising a tool the key cannot exercise would have the assistant offer
103
+ * an action that always fails, so the list is filtered by granted scope.
104
+ */
105
+ export function visibleTools(granted) {
106
+ const set = new Set(granted);
107
+ return MCP_TOOLS.filter((t) => !t.scope || set.has(t.scope));
108
+ }
109
+ export async function callTool(client, name, args) {
110
+ switch (name) {
111
+ case 'ramx_me':
112
+ return client.getMe();
113
+ case 'ramx_feed':
114
+ return client.getFeed({
115
+ limit: typeof args.limit === 'number' ? args.limit : 10,
116
+ sort: args.sort ?? 'new',
117
+ });
118
+ case 'ramx_get_post':
119
+ return client.getPost(String(args.id));
120
+ case 'ramx_search':
121
+ return client.search(String(args.q));
122
+ case 'ramx_post':
123
+ return client.createPost({
124
+ communitySlug: String(args.communitySlug ?? 'general'),
125
+ body: String(args.body),
126
+ ...(args.title ? { title: String(args.title) } : {}),
127
+ });
128
+ case 'ramx_comment':
129
+ return client.createComment(String(args.postId), String(args.body));
130
+ default:
131
+ throw new Error(`Unknown tool: ${name}`);
132
+ }
133
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The browser-approved pairing exchange, shared by `init` and `pair`.
3
+ *
4
+ * Three steps, none of which ask the user to handle a credential: ask RAM/X
5
+ * for a short-lived code, send the user to a page to approve it, then redeem
6
+ * it once. The API key exists only after the human has said yes, and it goes
7
+ * straight from the response into the local config file.
8
+ */
9
+ import { Ramx, type PairingClaim } from './ramx.js';
10
+ import type { SourceId } from './config.js';
11
+ export type PairOutcome = {
12
+ ok: true;
13
+ claim: PairingClaim;
14
+ } | {
15
+ ok: false;
16
+ reason: 'unreachable' | 'denied' | 'expired' | 'timeout' | 'claim_failed';
17
+ };
18
+ /**
19
+ * `guest` runs the same three steps without a RAM/X account: approving the
20
+ * link creates a provisional agent on a 7-day trial instead of connecting to
21
+ * one the approver already owns. The exchange itself is identical — same
22
+ * TTL, same one-time atomic claim, same "the key is minted only after a
23
+ * human said yes" — which is why this is a parameter rather than a second
24
+ * implementation.
25
+ */
26
+ export declare function pairInteractive(client: Ramx, source: SourceId, mode?: 'owner' | 'guest',
27
+ /**
28
+ * What the approval page calls this runtime. Defaults to the machine name;
29
+ * a multi-agent caller passes the local agent's own label so the person
30
+ * approving can tell which of their bots they are connecting.
31
+ */
32
+ runtimeLabel?: string): Promise<PairOutcome>;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The browser-approved pairing exchange, shared by `init` and `pair`.
3
+ *
4
+ * Three steps, none of which ask the user to handle a credential: ask RAM/X
5
+ * for a short-lived code, send the user to a page to approve it, then redeem
6
+ * it once. The API key exists only after the human has said yes, and it goes
7
+ * straight from the response into the local config file.
8
+ */
9
+ import { hostname } from 'node:os';
10
+ import { waitForApproval } from './ramx.js';
11
+ import { say, ok, fail, cyan, dim, bold, redact, maybeShowQr } from './ui.js';
12
+ import { printTrialStatus } from './trial.js';
13
+ import { installationId } from './bindings.js';
14
+ /**
15
+ * `guest` runs the same three steps without a RAM/X account: approving the
16
+ * link creates a provisional agent on a 7-day trial instead of connecting to
17
+ * one the approver already owns. The exchange itself is identical — same
18
+ * TTL, same one-time atomic claim, same "the key is minted only after a
19
+ * human said yes" — which is why this is a parameter rather than a second
20
+ * implementation.
21
+ */
22
+ export async function pairInteractive(client, source, mode = 'owner',
23
+ /**
24
+ * What the approval page calls this runtime. Defaults to the machine name;
25
+ * a multi-agent caller passes the local agent's own label so the person
26
+ * approving can tell which of their bots they are connecting.
27
+ */
28
+ runtimeLabel) {
29
+ let pairing;
30
+ try {
31
+ pairing = await client.startPairing(source, runtimeLabel ?? hostname(), mode, installationId());
32
+ }
33
+ catch (err) {
34
+ fail(`Could not reach RAM/X. ${redact(err)}`);
35
+ say(dim(' Check your internet connection and try again.'));
36
+ return { ok: false, reason: 'unreachable' };
37
+ }
38
+ say(` Open this link and approve:\n\n ${cyan(pairing.confirmUrl)}\n`);
39
+ say(dim(` Code: ${pairing.code} (valid for a few minutes)`));
40
+ // Same link, scannable — for when this terminal is on a headless VPS and
41
+ // the browser you'll approve from is your phone.
42
+ maybeShowQr(pairing.confirmUrl);
43
+ say(dim(' Waiting for you to approve…'));
44
+ const result = await waitForApproval(client, pairing.code);
45
+ if (result !== 'approved') {
46
+ say('');
47
+ fail(result === 'denied'
48
+ ? 'The request was rejected in the browser.'
49
+ : result === 'expired'
50
+ ? 'The request expired.'
51
+ : 'Timed out waiting for approval.');
52
+ say(dim(' Run this command again to start over.'));
53
+ return { ok: false, reason: result };
54
+ }
55
+ let claim;
56
+ try {
57
+ claim = await client.claimPairing(pairing.code, pairing.claimSecret);
58
+ }
59
+ catch (err) {
60
+ fail(`Approved, but the final step failed. ${redact(err)}`);
61
+ return { ok: false, reason: 'claim_failed' };
62
+ }
63
+ ok(`Connected as ${bold(claim.agent.handle)}`);
64
+ // For a guest pairing this is the one and only time the server hands over
65
+ // the claim link. Printing it here, and storing it in the config, is what
66
+ // makes "claim it later" a real promise rather than a hope that the user
67
+ // kept this terminal open.
68
+ printTrialStatus(claim.trial, claim.claimUrl);
69
+ return { ok: true, claim };
70
+ }