rakomi 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -12
- package/SECURITY.md +2 -2
- package/dist/billing-client.js +19 -0
- package/dist/client-registry.generated.js +156 -0
- package/dist/clients.js +10 -2
- package/dist/commands/connect.js +85 -29
- package/dist/commands/login.js +1 -0
- package/dist/commands/tenants.js +75 -1
- package/dist/commands/upgrade.js +21 -0
- package/dist/commands/use.js +23 -7
- package/dist/commands/whoami.js +25 -5
- package/dist/env.js +5 -1
- package/dist/http.js +58 -1
- package/dist/index.js +68 -8
- package/dist/lease-client.js +64 -0
- package/dist/loopback-server.js +85 -6
- package/dist/mcp-config.js +52 -29
- package/dist/server-name.js +15 -0
- package/dist/tenant-config.js +15 -4
- package/dist/tenants-client.js +43 -0
- package/dist/token-refresh.js +56 -0
- package/dist/toml-config.js +96 -0
- package/dist/usage.js +17 -6
- package/package.json +2 -2
- package/sbom.cdx.json +3 -3
package/README.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
The [Rakomi](https://rakomi.com) CLI. EU-native authentication as a service.
|
|
4
4
|
|
|
5
|
+
## Ask your agent
|
|
6
|
+
|
|
7
|
+
Add the Rakomi MCP server to your coding agent, then ask it directly:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
claude mcp add --transport http rakomi https://mcp.rakomi.com/mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
"List the AI agents connected to my Rakomi tenant."
|
|
14
|
+
|
|
5
15
|
## Getting started
|
|
6
16
|
|
|
7
17
|
```sh
|
|
@@ -15,9 +25,12 @@ session to get a short code to enter on another device instead. If you don't alr
|
|
|
15
25
|
Rakomi administrator for the right value.
|
|
16
26
|
|
|
17
27
|
`rakomi whoami` shows the tenant you signed in against ("Home tenant") and the tenant your other
|
|
18
|
-
commands should treat as active ("Active tenant")
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
commands should treat as active ("Active tenant") — including whether that active selection was
|
|
29
|
+
confirmed by the server or only remembered locally. `rakomi tenants memberships` lists every tenant
|
|
30
|
+
your account is a verified member of, with your role in each. `rakomi use <slug>` looks one up from
|
|
31
|
+
that list and remembers it — a slug you don't belong to fails with a plain "not found" rather than
|
|
32
|
+
confirming the tenant exists; `rakomi use <tenant-id>` (a full id) remembers it directly without a
|
|
33
|
+
lookup, and `--tenant <tenant-id>` overrides it for one command.
|
|
21
34
|
|
|
22
35
|
## Connect an AI agent
|
|
23
36
|
|
|
@@ -25,10 +38,12 @@ grant — the tenant `whoami` shows as your Home tenant is the only one the serv
|
|
|
25
38
|
npx rakomi connect
|
|
26
39
|
```
|
|
27
40
|
|
|
28
|
-
Detects Claude Code and writes
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
Detects Claude Code and writes its config entry for you. Pass `--client <name>` for any other known
|
|
42
|
+
client — a config file is written in that client's own shape for one that supports it, or its own
|
|
43
|
+
connection steps are printed for a UI-only client (Claude Desktop, ChatGPT, JetBrains). Run
|
|
44
|
+
`rakomi --help` for the current client list. Read access works immediately once the client finishes
|
|
45
|
+
its own sign-in; write access is a separate step your tenant owner approves — see the
|
|
46
|
+
[Connect an AI Agent guide](https://docs.rakomi.dev/guides/connect-an-ai-agent).
|
|
32
47
|
|
|
33
48
|
Pass `--cimd-url <url>` (find it in your MCP client's own connection diagnostics) and `rakomi`
|
|
34
49
|
confirms the connection for you instead of leaving you to check the dashboard; add `--write` to
|
|
@@ -54,10 +69,11 @@ token), sends a real invitation e-mail, and is refused under `--ci` — see
|
|
|
54
69
|
| `login` | sign in (browser by default; `--no-browser` for a device code) |
|
|
55
70
|
| `logout` | clear the local session |
|
|
56
71
|
| `whoami` | show the signed-in account, home tenant, and active tenant |
|
|
57
|
-
| `use <tenant-id>` | remember a tenant
|
|
58
|
-
| `connect` | connect Claude Code
|
|
72
|
+
| `use <tenant-id-or-slug>` | remember a tenant locally for `whoami`/future commands; a slug is server-verified |
|
|
73
|
+
| `connect` | connect an MCP client (Claude Code, Claude Desktop, and others — see `--help`) to your tenant (read access) |
|
|
59
74
|
| `tenants create <name>` | create a tenant (`--owner me\|<email>`, `--slug <slug>`) |
|
|
60
75
|
| `tenants list` | list tenants you provisioned |
|
|
76
|
+
| `tenants memberships` | list tenants you're a verified member of |
|
|
61
77
|
|
|
62
78
|
## Global options
|
|
63
79
|
|
|
@@ -70,10 +86,11 @@ token), sends a real invitation e-mail, and is refused under `--ci` — see
|
|
|
70
86
|
| `--no-keychain` | store the session in a `0600` file instead of the OS keychain |
|
|
71
87
|
| `--tenant-id <uuid>` | (`login`) the tenant to sign in against, if `RAKOMI_PLATFORM_TENANT_ID` isn't set for you |
|
|
72
88
|
| `--tenant <tenant-id>` | override the active tenant for a single command (`whoami` today) |
|
|
73
|
-
| `--client <name>` |
|
|
74
|
-
| `--
|
|
89
|
+
| `--client <name>` | one of the known MCP clients (`connect --help` prints the current list) — required when more than one applies |
|
|
90
|
+
| `--name <server-name>` | (`connect`) the config key to write the entry under (default `rakomi`) — one name per workspace on a machine; a client keeps one login per server name |
|
|
91
|
+
| `--undo` | restore the config file `connect` last backed up (for the same `--name`) |
|
|
75
92
|
| `--cimd-url <url>` | (`connect`) the connecting client's own CIMD document URL — confirms/re-checks the connection |
|
|
76
|
-
| `--status` | (`connect`) re-check status only — needs `--cimd-url`, never rewrites
|
|
93
|
+
| `--status` | (`connect`) re-check status only — needs `--cimd-url`, never rewrites the config file |
|
|
77
94
|
| `-h`, `--help` | show help |
|
|
78
95
|
| `-V`, `--version` | print the version |
|
|
79
96
|
|
package/SECURITY.md
CHANGED
|
@@ -9,8 +9,8 @@ While these packages remain pre-1.0 (`0.x`), they carry **no stability or suppor
|
|
|
9
9
|
(SemVer 2.0.0 §4); the latest `0.x` line receives security updates on a best-effort basis.
|
|
10
10
|
|
|
11
11
|
From version **1.0** onward, Rakomi maintains the current (N) and previous (N-1) MAJOR in parallel, with N-1 receiving
|
|
12
|
-
security-only fixes.
|
|
13
|
-
**
|
|
12
|
+
security-only fixes. For each MAJOR version of the Rakomi SDKs, CRE8EVE commits to a CRA support period of
|
|
13
|
+
**at least five years (60 months)**, meeting the support-period requirement of **CRA Art. 13(8)**. The authoritative, machine-readable support windows are published at
|
|
14
14
|
[`https://api.rakomi.com/.well-known/sdk-support.json`](https://api.rakomi.com/.well-known/sdk-support.json)
|
|
15
15
|
and rendered for humans on the [SDK Support & Lifecycle page](https://rakomi.com/sdk-support). This
|
|
16
16
|
document points at that single source rather than re-typing dated rows.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { CliError, EXIT } from './errors.js';
|
|
2
|
+
import { describeError, request } from './http.js';
|
|
3
|
+
export async function getBillingPlan(deps, opts) {
|
|
4
|
+
const url = new URL('/v1/billing/plan', opts.apiBaseUrl);
|
|
5
|
+
url.searchParams.set('tenant_id', opts.tenantId);
|
|
6
|
+
const result = await request(deps, {
|
|
7
|
+
method: 'GET',
|
|
8
|
+
url: url.toString(),
|
|
9
|
+
headers: opts.dpop ? undefined : { authorization: `Bearer ${opts.accessToken}` },
|
|
10
|
+
dpop: opts.dpop ? { key: opts.dpop, accessToken: opts.accessToken } : undefined,
|
|
11
|
+
});
|
|
12
|
+
if (result.status === 401) {
|
|
13
|
+
throw new CliError('Your session has expired. Run `rakomi login` again.', EXIT.NOT_LOGGED_IN);
|
|
14
|
+
}
|
|
15
|
+
if (result.status !== 200) {
|
|
16
|
+
throw new CliError(`Could not fetch your billing plan: ${describeError(result.body, result.status)}`, EXIT.FAIL);
|
|
17
|
+
}
|
|
18
|
+
return result.body;
|
|
19
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export const RAKOMI_SERVER_NAME = "rakomi";
|
|
2
|
+
export const CLIENT_REGISTRY = [
|
|
3
|
+
{
|
|
4
|
+
id: "claude-code",
|
|
5
|
+
displayName: "Claude Code",
|
|
6
|
+
tier: "written",
|
|
7
|
+
status: "verified",
|
|
8
|
+
finishInstruction: "1. Run `claude` in this directory and approve the \"{{SERVER_NAME}}\" project server when prompted.\n2. Then run `claude mcp login {{SERVER_NAME}}` to finish sign-in in your browser (or use `/mcp` inside that session).",
|
|
9
|
+
config: {
|
|
10
|
+
scope: "project",
|
|
11
|
+
pathTemplate: ".mcp.json",
|
|
12
|
+
serialization: "json",
|
|
13
|
+
topLevelKey: "mcpServers",
|
|
14
|
+
remoteUrlField: "url",
|
|
15
|
+
extraFields: { "type": "http" },
|
|
16
|
+
},
|
|
17
|
+
evidenceUrl: "https://code.claude.com/docs/en/mcp",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "vscode",
|
|
21
|
+
displayName: "VS Code",
|
|
22
|
+
tier: "written",
|
|
23
|
+
status: "documented-shape-only",
|
|
24
|
+
finishInstruction: "Open the Command Palette and run \"MCP: List Servers\", or the Extensions view — VS Code starts the OAuth flow automatically the first time it connects.",
|
|
25
|
+
config: {
|
|
26
|
+
scope: "project",
|
|
27
|
+
pathTemplate: ".vscode/mcp.json",
|
|
28
|
+
serialization: "json",
|
|
29
|
+
topLevelKey: "servers",
|
|
30
|
+
remoteUrlField: "url",
|
|
31
|
+
extraFields: { "type": "http" },
|
|
32
|
+
},
|
|
33
|
+
evidenceUrl: "https://code.visualstudio.com/docs/agents/reference/mcp-configuration",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "cursor",
|
|
37
|
+
displayName: "Cursor",
|
|
38
|
+
tier: "written",
|
|
39
|
+
status: "documented-shape-only",
|
|
40
|
+
finishInstruction: "Open Cursor's MCP settings and connect — for a server that supports OAuth, Cursor completes the flow for you (or accepts static client credentials in mcp.json instead of dynamic client registration).",
|
|
41
|
+
config: {
|
|
42
|
+
scope: "project",
|
|
43
|
+
pathTemplate: ".cursor/mcp.json",
|
|
44
|
+
serialization: "json",
|
|
45
|
+
topLevelKey: "mcpServers",
|
|
46
|
+
remoteUrlField: "url",
|
|
47
|
+
extraFields: {},
|
|
48
|
+
},
|
|
49
|
+
evidenceUrl: "https://cursor.com/docs/mcp",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "antigravity",
|
|
53
|
+
displayName: "Google Antigravity",
|
|
54
|
+
tier: "written",
|
|
55
|
+
status: "documented-shape-only",
|
|
56
|
+
finishInstruction: "Open the MCP Store or your MCP settings in Antigravity — it handles OAuth automatically for servers that support dynamic client registration (DCR).",
|
|
57
|
+
config: {
|
|
58
|
+
scope: "project",
|
|
59
|
+
pathTemplate: ".agents/mcp_config.json",
|
|
60
|
+
serialization: "json",
|
|
61
|
+
topLevelKey: "mcpServers",
|
|
62
|
+
remoteUrlField: "serverUrl",
|
|
63
|
+
extraFields: {},
|
|
64
|
+
},
|
|
65
|
+
evidenceUrl: "https://antigravity.google/docs/ide/mcp/",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "gemini-cli",
|
|
69
|
+
displayName: "Gemini CLI",
|
|
70
|
+
tier: "written",
|
|
71
|
+
status: "documented-shape-only",
|
|
72
|
+
finishInstruction: "Run `gemini mcp list`, or just start a session — the Gemini CLI supports OAuth 2.0 for remote MCP servers and negotiates it automatically.",
|
|
73
|
+
config: {
|
|
74
|
+
scope: "project",
|
|
75
|
+
pathTemplate: ".gemini/settings.json",
|
|
76
|
+
serialization: "json",
|
|
77
|
+
topLevelKey: "mcpServers",
|
|
78
|
+
remoteUrlField: "httpUrl",
|
|
79
|
+
extraFields: {},
|
|
80
|
+
},
|
|
81
|
+
evidenceUrl: "https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "zed",
|
|
85
|
+
displayName: "Zed",
|
|
86
|
+
tier: "written",
|
|
87
|
+
status: "documented-shape-only",
|
|
88
|
+
finishInstruction: "Open Settings -> AI -> MCP Servers in Zed — when a remote server has no Authorization header configured, Zed prompts you to authenticate with the standard MCP OAuth flow.",
|
|
89
|
+
config: {
|
|
90
|
+
scope: "project",
|
|
91
|
+
pathTemplate: ".zed/settings.json",
|
|
92
|
+
serialization: "json",
|
|
93
|
+
topLevelKey: "context_servers",
|
|
94
|
+
remoteUrlField: "url",
|
|
95
|
+
extraFields: {},
|
|
96
|
+
},
|
|
97
|
+
evidenceUrl: "https://zed.dev/docs/ai/mcp",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "codex-cli",
|
|
101
|
+
displayName: "OpenAI Codex CLI",
|
|
102
|
+
tier: "written",
|
|
103
|
+
status: "documented-shape-only",
|
|
104
|
+
finishInstruction: "Run `codex mcp login {{SERVER_NAME}}` to finish sign-in in your browser.",
|
|
105
|
+
config: {
|
|
106
|
+
scope: "user",
|
|
107
|
+
pathTemplate: ".codex/config.toml",
|
|
108
|
+
serialization: "toml",
|
|
109
|
+
topLevelKey: "mcp_servers",
|
|
110
|
+
remoteUrlField: "url",
|
|
111
|
+
extraFields: {},
|
|
112
|
+
},
|
|
113
|
+
evidenceUrl: "https://learn.chatgpt.com/docs/extend/mcp?surface=cli",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "devin-desktop",
|
|
117
|
+
displayName: "Devin Desktop",
|
|
118
|
+
tier: "written",
|
|
119
|
+
status: "documented-shape-only",
|
|
120
|
+
finishInstruction: "Open Devin Desktop — it supports OAuth for each transport type and prompts you to connect the first time.",
|
|
121
|
+
config: {
|
|
122
|
+
scope: "user",
|
|
123
|
+
pathTemplate: ".codeium/windsurf/mcp_config.json",
|
|
124
|
+
serialization: "json",
|
|
125
|
+
topLevelKey: "mcpServers",
|
|
126
|
+
remoteUrlField: "serverUrl",
|
|
127
|
+
extraFields: {},
|
|
128
|
+
},
|
|
129
|
+
evidenceUrl: "https://docs.devin.ai/desktop/cascade/mcp",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "claude-desktop",
|
|
133
|
+
displayName: "Claude Desktop",
|
|
134
|
+
tier: "instructed",
|
|
135
|
+
status: "verified",
|
|
136
|
+
finishInstruction: "Claude Desktop connects to remote MCP servers through Connectors, configured from your Claude account rather than a local file — there is nothing for `rakomi connect` to write.\n\n1. Open Settings -> Connectors in Claude Desktop and add a custom connector.\n2. Enter Rakomi's MCP server URL: {{MCP_URL}}\n3. Click Connect. Claude Desktop opens your browser at accounts.rakomi.com — sign in and\n approve the read-only consent screen.",
|
|
137
|
+
evidenceUrl: "https://support.claude.com/en/articles/11175166-about-custom-connectors-remote-mcp",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "chatgpt",
|
|
141
|
+
displayName: "ChatGPT",
|
|
142
|
+
tier: "instructed",
|
|
143
|
+
status: "documented-shape-only",
|
|
144
|
+
finishInstruction: "In ChatGPT, go to Workspace Settings -> Permissions & Roles -> Developer mode, then add {{MCP_URL}} as a remote server. ChatGPT starts an OAuth flow to your workspace's identity provider once it's added.",
|
|
145
|
+
evidenceUrl: "https://developers.openai.com/api/docs/mcp",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "jetbrains",
|
|
149
|
+
displayName: "JetBrains AI Assistant",
|
|
150
|
+
tier: "instructed",
|
|
151
|
+
status: "documented-shape-only",
|
|
152
|
+
finishInstruction: "In your JetBrains IDE, go to Settings -> Tools -> AI Assistant -> Model Context Protocol (MCP), click Add, and paste: {\"mcpServers\":{\"{{SERVER_NAME}}\":{\"url\":\"{{MCP_URL}}\"}}}\nJetBrains' own documentation does not state whether it drives OAuth for you — be ready to complete sign-in in whatever browser tab it opens.",
|
|
153
|
+
evidenceUrl: "https://www.jetbrains.com/help/ai-assistant/mcp.html",
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
export const KNOWN_CLIENTS = CLIENT_REGISTRY.map((c) => c.id);
|
package/dist/clients.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import { CLIENT_REGISTRY, KNOWN_CLIENTS } from './client-registry.generated.js';
|
|
2
|
+
import { CliError, EXIT } from './errors.js';
|
|
3
|
+
export { CLIENT_REGISTRY, KNOWN_CLIENTS };
|
|
2
4
|
export function isKnownClient(value) {
|
|
3
5
|
return KNOWN_CLIENTS.includes(value);
|
|
4
6
|
}
|
|
7
|
+
export function clientRegistryEntry(id) {
|
|
8
|
+
const entry = CLIENT_REGISTRY.find((c) => c.id === id);
|
|
9
|
+
if (!entry)
|
|
10
|
+
throw new CliError(`clientRegistryEntry: unknown client '${id}' — this should be unreachable, isKnownClient() gates every caller`, EXIT.FAIL);
|
|
11
|
+
return entry;
|
|
12
|
+
}
|
|
5
13
|
export function clientDisplayName(client) {
|
|
6
|
-
return client
|
|
14
|
+
return clientRegistryEntry(client).displayName;
|
|
7
15
|
}
|
|
8
16
|
export function resolveTargetClients(opts) {
|
|
9
17
|
if (opts.explicitClient) {
|
package/dist/commands/connect.js
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
|
-
import { AmbiguousClientError,
|
|
1
|
+
import { AmbiguousClientError, clientRegistryEntry, resolveTargetClients } from '../clients.js';
|
|
2
2
|
import { pollCimdMaterialization, requestWriteElevation } from '../connect-client.js';
|
|
3
|
-
import { DEFAULT_MCP_URL } from '../env.js';
|
|
4
3
|
import { InteractiveRequiredError, NotLoggedInError, UsageError } from '../errors.js';
|
|
5
4
|
import { resolveDpopKey } from '../install-key.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
'',
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
import { backupSuffixFor, clientConfigPath, RAKOMI_SERVER_NAME, undoClientConfigWrite, writeClientMcpConfigEntry, } from '../mcp-config.js';
|
|
6
|
+
import { resolveServerName } from '../server-name.js';
|
|
7
|
+
import { undoClientTomlConfigWrite, writeClientTomlConfigEntry } from '../toml-config.js';
|
|
8
|
+
function renderInstruction(instruction, mcpUrl, serverName) {
|
|
9
|
+
return instruction.replaceAll('{{MCP_URL}}', mcpUrl).replaceAll('{{SERVER_NAME}}', serverName);
|
|
10
|
+
}
|
|
11
|
+
function resolveSingleTargetClient(deps) {
|
|
12
|
+
let targets;
|
|
13
|
+
try {
|
|
14
|
+
targets = resolveTargetClients({ explicitClient: deps.explicitClient, claudeCodeDetected: deps.detectClaudeCode() });
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
if (e instanceof AmbiguousClientError)
|
|
18
|
+
throw new UsageError(e.message);
|
|
19
|
+
throw e;
|
|
20
|
+
}
|
|
21
|
+
return clientRegistryEntry(targets[0]);
|
|
22
|
+
}
|
|
16
23
|
export async function runConnect(deps) {
|
|
24
|
+
const serverName = resolveServerName(deps.serverName);
|
|
17
25
|
if (deps.undo) {
|
|
18
|
-
|
|
19
|
-
deps.stdout.write(restored
|
|
20
|
-
? `Restored ${mcpConfigPath(deps.cwd)} from the backup \`rakomi connect\` made.\n`
|
|
21
|
-
: `No \`rakomi connect\` backup found at ${mcpConfigPath(deps.cwd)}.rakomi-backup — nothing to undo.\n`);
|
|
26
|
+
runUndo(deps, serverName);
|
|
22
27
|
return;
|
|
23
28
|
}
|
|
24
29
|
const session = deps.session.read();
|
|
@@ -37,13 +42,33 @@ export async function runConnect(deps) {
|
|
|
37
42
|
throw new UsageError(e.message);
|
|
38
43
|
throw e;
|
|
39
44
|
}
|
|
40
|
-
for (const
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
for (const clientId of targets) {
|
|
46
|
+
const client = clientRegistryEntry(clientId);
|
|
47
|
+
if (client.tier === 'instructed') {
|
|
48
|
+
deps.stdout.write(`${client.displayName}:\n${renderInstruction(client.finishInstruction, deps.mcpUrl, serverName)}\n`);
|
|
43
49
|
continue;
|
|
44
50
|
}
|
|
45
|
-
|
|
51
|
+
if (clientId === 'claude-code') {
|
|
52
|
+
await connectClaudeCode(deps, session, client, serverName);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
connectGenericWrittenClient(deps, client, serverName);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function runUndo(deps, serverName) {
|
|
60
|
+
const client = resolveSingleTargetClient(deps);
|
|
61
|
+
if (!client.config) {
|
|
62
|
+
deps.stdout.write(`${client.displayName} writes no config file — nothing to undo.\n`);
|
|
63
|
+
return;
|
|
46
64
|
}
|
|
65
|
+
const path = clientConfigPath(client, deps.cwd, deps.homeDir);
|
|
66
|
+
const restored = client.config.serialization === 'toml'
|
|
67
|
+
? undoClientTomlConfigWrite(client, deps.cwd, deps.homeDir, serverName)
|
|
68
|
+
: undoClientConfigWrite(client, deps.cwd, deps.homeDir, serverName);
|
|
69
|
+
deps.stdout.write(restored
|
|
70
|
+
? `Restored ${path} from the backup \`rakomi connect\` made.\n`
|
|
71
|
+
: `No \`rakomi connect\` backup found at ${path}${backupSuffixFor(serverName)} — nothing to undo.\n`);
|
|
47
72
|
}
|
|
48
73
|
async function reportStatus(deps, session) {
|
|
49
74
|
if (!deps.cimdUrl) {
|
|
@@ -65,20 +90,35 @@ async function reportStatus(deps, session) {
|
|
|
65
90
|
await requestWrite(deps, session, row.id);
|
|
66
91
|
}
|
|
67
92
|
}
|
|
68
|
-
|
|
69
|
-
if (deps.dryRun) {
|
|
70
|
-
deps.stdout.write(`Would write ${mcpConfigPath(deps.cwd)} with mcpServers.rakomi -> ${deps.mcpUrl}. No file written (--dry-run).\n`);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const result = writeRakomiMcpServerEntry(deps.cwd, deps.mcpUrl);
|
|
93
|
+
function writeSummary(deps, result) {
|
|
74
94
|
if (result.wrote) {
|
|
75
|
-
deps.stdout.write(`Wrote ${
|
|
95
|
+
deps.stdout.write(`Wrote ${result.path}${result.backedUp ? ' (previous file backed up)' : ''}.\n${result.diff}\n`);
|
|
76
96
|
}
|
|
77
97
|
else {
|
|
78
98
|
deps.stdout.write(`${result.diff}\n`);
|
|
79
99
|
}
|
|
80
|
-
|
|
100
|
+
}
|
|
101
|
+
function secondWorkspaceHint(client, explicitClient) {
|
|
102
|
+
const example = `${RAKOMI_SERVER_NAME}-<workspace>`;
|
|
103
|
+
const clientFlag = explicitClient ? ` --client ${explicitClient}` : '';
|
|
104
|
+
const where = client.config?.scope === 'user' ? '' : ' in that project';
|
|
105
|
+
const login = client.id === 'claude-code' ? `, then \`claude mcp login ${example}\`` : ', then sign in under that name';
|
|
106
|
+
return (`\nTip: another project on this machine bound to a different workspace? Sign-ins are kept per server ` +
|
|
107
|
+
`name, so give it its own: run \`rakomi connect${clientFlag} --name ${example}\`${where}${login}.\n`);
|
|
108
|
+
}
|
|
109
|
+
async function connectClaudeCode(deps, session, client, serverName) {
|
|
110
|
+
const path = clientConfigPath(client, deps.cwd, deps.homeDir);
|
|
111
|
+
if (deps.dryRun) {
|
|
112
|
+
deps.stdout.write(`Would write ${path} with ${client.config.topLevelKey}.${serverName} -> ${deps.mcpUrl}. No file written (--dry-run).\n`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const result = writeClientMcpConfigEntry(client, deps.cwd, deps.homeDir, deps.mcpUrl, serverName);
|
|
116
|
+
writeSummary(deps, result);
|
|
117
|
+
deps.stdout.write(`${renderInstruction(client.finishInstruction, deps.mcpUrl, serverName)}\n` +
|
|
81
118
|
'Once approved, Claude Code has READ access to your tenant immediately.\n');
|
|
119
|
+
if (result.wrote && serverName === RAKOMI_SERVER_NAME) {
|
|
120
|
+
deps.stdout.write(secondWorkspaceHint(client, deps.explicitClient));
|
|
121
|
+
}
|
|
82
122
|
if (!deps.cimdUrl) {
|
|
83
123
|
deps.stdout.write('\nOnce connected, Rakomi can confirm it and (with --write) request write access for you — pass ' +
|
|
84
124
|
'`--cimd-url <url>` (find it in your MCP client\'s own connection diagnostics) to use it, or ' +
|
|
@@ -93,8 +133,9 @@ async function connectClaudeCode(deps, session) {
|
|
|
93
133
|
dpop,
|
|
94
134
|
});
|
|
95
135
|
if (!row) {
|
|
136
|
+
const nameFlag = serverName === RAKOMI_SERVER_NAME ? '' : ` --name ${serverName}`;
|
|
96
137
|
deps.stdout.write(`\nNot connected yet. Ctrl+C is safe — resume any time with:\n` +
|
|
97
|
-
` rakomi connect --status --cimd-url ${deps.cimdUrl}\n`);
|
|
138
|
+
` rakomi connect --status${nameFlag} --cimd-url ${deps.cimdUrl}\n`);
|
|
98
139
|
return;
|
|
99
140
|
}
|
|
100
141
|
deps.stdout.write(`\nConnected — access: ${row.agent_access.level}.\n`);
|
|
@@ -105,6 +146,21 @@ async function connectClaudeCode(deps, session) {
|
|
|
105
146
|
await requestWrite(deps, session, row.id);
|
|
106
147
|
}
|
|
107
148
|
}
|
|
149
|
+
function connectGenericWrittenClient(deps, client, serverName) {
|
|
150
|
+
const path = clientConfigPath(client, deps.cwd, deps.homeDir);
|
|
151
|
+
if (deps.dryRun) {
|
|
152
|
+
deps.stdout.write(`Would write ${path} with ${client.config.topLevelKey}.${serverName} -> ${deps.mcpUrl}. No file written (--dry-run).\n`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const result = client.config.serialization === 'toml'
|
|
156
|
+
? writeClientTomlConfigEntry(client, deps.cwd, deps.homeDir, deps.mcpUrl, serverName)
|
|
157
|
+
: writeClientMcpConfigEntry(client, deps.cwd, deps.homeDir, deps.mcpUrl, serverName);
|
|
158
|
+
writeSummary(deps, result);
|
|
159
|
+
deps.stdout.write(`${renderInstruction(client.finishInstruction, deps.mcpUrl, serverName)}\n`);
|
|
160
|
+
if (result.wrote && serverName === RAKOMI_SERVER_NAME) {
|
|
161
|
+
deps.stdout.write(secondWorkspaceHint(client, deps.explicitClient));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
108
164
|
async function requestWrite(deps, session, oauthClientId) {
|
|
109
165
|
const dpop = resolveDpopKey(deps.keys, session);
|
|
110
166
|
await requestWriteElevation(deps, { apiBaseUrl: deps.apiBaseUrl, accessToken: session.access_token, oauthClientId, dpop });
|
package/dist/commands/login.js
CHANGED
|
@@ -89,6 +89,7 @@ async function runLoopbackLogin(deps, apiUrl, cid, installKey, tenantId) {
|
|
|
89
89
|
scope: LOGIN_SCOPE,
|
|
90
90
|
tenantId,
|
|
91
91
|
});
|
|
92
|
+
deps.stdout.write('Signing you in to your Rakomi developer account (separate from your tenants\' end-user accounts)…\n');
|
|
92
93
|
deps.stdout.write(`Opening your browser to sign in:\n ${authorizeUrl}\n`);
|
|
93
94
|
await deps.openBrowser(authorizeUrl);
|
|
94
95
|
const callback = await listener.waitForCallback(LOOPBACK_TIMEOUT_MS);
|
package/dist/commands/tenants.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { InteractiveRequiredError, NotLoggedInError, UsageError } from '../errors.js';
|
|
2
2
|
import { resolveDpopKey } from '../install-key.js';
|
|
3
|
-
import {
|
|
3
|
+
import { claimEphemeralTenant, EPHEMERAL_LEASE_LABEL_MAX_LENGTH, EPHEMERAL_TENANT_TTL_SECONDS_MAX, EPHEMERAL_TENANT_TTL_SECONDS_MIN, releaseEphemeralTenant, } from '../lease-client.js';
|
|
4
|
+
import { createTenant, listTenantMemberships, listTenants } from '../tenants-client.js';
|
|
4
5
|
export async function runTenantsCreate(deps, args) {
|
|
5
6
|
const session = deps.session.read();
|
|
6
7
|
if (!session)
|
|
@@ -59,3 +60,76 @@ export async function runTenantsList(deps) {
|
|
|
59
60
|
}
|
|
60
61
|
deps.stdout.write(result.data.map((t) => `${t.slug}\t${t.id}\t${t.status}`).join('\n') + '\n');
|
|
61
62
|
}
|
|
63
|
+
export async function runTenantsMemberships(deps) {
|
|
64
|
+
const session = deps.session.read();
|
|
65
|
+
if (!session)
|
|
66
|
+
throw new NotLoggedInError();
|
|
67
|
+
const result = await listTenantMemberships(deps, {
|
|
68
|
+
apiBaseUrl: session.api_base_url,
|
|
69
|
+
accessToken: session.access_token,
|
|
70
|
+
dpop: resolveDpopKey(deps.keys, session),
|
|
71
|
+
});
|
|
72
|
+
if (deps.json) {
|
|
73
|
+
deps.stdout.write(JSON.stringify(result) + '\n');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (result.data.length === 0) {
|
|
77
|
+
deps.stdout.write('No tenant memberships found for your account.\n');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
deps.stdout.write(result.data.map((m) => `${m.slug}\t${m.id}\t${m.role}`).join('\n') + '\n');
|
|
81
|
+
}
|
|
82
|
+
function resolveParentTenantId(explicit, tenantConfig) {
|
|
83
|
+
return explicit ?? tenantConfig.read() ?? undefined;
|
|
84
|
+
}
|
|
85
|
+
const NO_PARENT_TENANT_MESSAGE = 'No tenant to operate on — pass --tenant <tenant-id>, or run `rakomi use <tenant-id>` first.';
|
|
86
|
+
export async function runTenantsClaim(deps, args) {
|
|
87
|
+
const session = deps.session.read();
|
|
88
|
+
if (!session)
|
|
89
|
+
throw new NotLoggedInError('Not logged in. Run `rakomi login --ci` first.');
|
|
90
|
+
const parentTenantId = resolveParentTenantId(args.parentTenantId, deps.tenantConfig);
|
|
91
|
+
if (!parentTenantId)
|
|
92
|
+
throw new UsageError(NO_PARENT_TENANT_MESSAGE);
|
|
93
|
+
if (args.ttlSeconds !== undefined &&
|
|
94
|
+
(args.ttlSeconds < EPHEMERAL_TENANT_TTL_SECONDS_MIN || args.ttlSeconds > EPHEMERAL_TENANT_TTL_SECONDS_MAX)) {
|
|
95
|
+
throw new UsageError(`--ttl-seconds must be between ${EPHEMERAL_TENANT_TTL_SECONDS_MIN} and ${EPHEMERAL_TENANT_TTL_SECONDS_MAX}, got ${args.ttlSeconds}`);
|
|
96
|
+
}
|
|
97
|
+
const label = args.label?.trim();
|
|
98
|
+
if (args.label !== undefined && (label === '' || (label?.length ?? 0) > EPHEMERAL_LEASE_LABEL_MAX_LENGTH)) {
|
|
99
|
+
throw new UsageError(`--label must be 1-${EPHEMERAL_LEASE_LABEL_MAX_LENGTH} characters after trimming, got "${args.label}"`);
|
|
100
|
+
}
|
|
101
|
+
const result = await claimEphemeralTenant(deps, {
|
|
102
|
+
apiBaseUrl: session.api_base_url,
|
|
103
|
+
accessToken: session.access_token,
|
|
104
|
+
parentTenantId,
|
|
105
|
+
ttlSeconds: args.ttlSeconds,
|
|
106
|
+
label,
|
|
107
|
+
dpop: resolveDpopKey(deps.keys, session),
|
|
108
|
+
});
|
|
109
|
+
if (deps.json) {
|
|
110
|
+
deps.stdout.write(JSON.stringify(result) + '\n');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
deps.stdout.write(`Claimed ephemeral tenant "${result.slug}" (${result.tenant_id}) — expires ${result.expires_at} (source: ${result.source})\n` +
|
|
114
|
+
`Lease id: ${result.lease_id} — pass this to \`rakomi tenants release --ci\` when you're done with it.\n`);
|
|
115
|
+
}
|
|
116
|
+
export async function runTenantsRelease(deps, args) {
|
|
117
|
+
const session = deps.session.read();
|
|
118
|
+
if (!session)
|
|
119
|
+
throw new NotLoggedInError('Not logged in. Run `rakomi login --ci` first.');
|
|
120
|
+
const parentTenantId = resolveParentTenantId(args.parentTenantId, deps.tenantConfig);
|
|
121
|
+
if (!parentTenantId)
|
|
122
|
+
throw new UsageError(NO_PARENT_TENANT_MESSAGE);
|
|
123
|
+
await releaseEphemeralTenant(deps, {
|
|
124
|
+
apiBaseUrl: session.api_base_url,
|
|
125
|
+
accessToken: session.access_token,
|
|
126
|
+
parentTenantId,
|
|
127
|
+
tenantId: args.tenantId,
|
|
128
|
+
dpop: resolveDpopKey(deps.keys, session),
|
|
129
|
+
});
|
|
130
|
+
if (deps.json) {
|
|
131
|
+
deps.stdout.write(JSON.stringify({ released: true, tenant_id: args.tenantId }) + '\n');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
deps.stdout.write(`Released ephemeral tenant ${args.tenantId} — deletion scheduled.\n`);
|
|
135
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getBillingPlan } from '../billing-client.js';
|
|
2
|
+
import { NotLoggedInError, UsageError } from '../errors.js';
|
|
3
|
+
import { resolveDpopKey } from '../install-key.js';
|
|
4
|
+
export async function runUpgrade(deps) {
|
|
5
|
+
const session = deps.session.read();
|
|
6
|
+
if (!session)
|
|
7
|
+
throw new NotLoggedInError();
|
|
8
|
+
const tenantId = deps.explicitTenant ?? deps.tenantConfig.read() ?? undefined;
|
|
9
|
+
if (!tenantId) {
|
|
10
|
+
throw new UsageError('No active tenant set. Run `rakomi use <tenant-slug>` first, or pass --tenant <id>.');
|
|
11
|
+
}
|
|
12
|
+
const dpop = resolveDpopKey(deps.keys, session);
|
|
13
|
+
const plan = await getBillingPlan(deps, {
|
|
14
|
+
apiBaseUrl: session.api_base_url,
|
|
15
|
+
accessToken: session.access_token,
|
|
16
|
+
tenantId,
|
|
17
|
+
dpop,
|
|
18
|
+
});
|
|
19
|
+
deps.stdout.write(`Current plan: ${plan.effective_plan}\nOpening ${plan.upgrade_url} in your browser...\n`);
|
|
20
|
+
await deps.openBrowser(plan.upgrade_url);
|
|
21
|
+
}
|
package/dist/commands/use.js
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { isValidTenantId } from '../env.js';
|
|
2
|
-
import { UsageError } from '../errors.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { NotLoggedInError, UsageError } from '../errors.js';
|
|
3
|
+
import { resolveDpopKey } from '../install-key.js';
|
|
4
|
+
import { getTenantMembershipBySlug } from '../tenants-client.js';
|
|
5
|
+
export async function runUse(deps, args) {
|
|
6
|
+
if (isValidTenantId(args.tenantId)) {
|
|
7
|
+
deps.tenantConfig.write(args.tenantId);
|
|
8
|
+
deps.stdout.write(`Active tenant set to ${args.tenantId}.\n`);
|
|
9
|
+
return;
|
|
7
10
|
}
|
|
8
|
-
deps.
|
|
9
|
-
|
|
11
|
+
const session = deps.session.read();
|
|
12
|
+
if (!session)
|
|
13
|
+
throw new NotLoggedInError();
|
|
14
|
+
const membership = await getTenantMembershipBySlug(deps, {
|
|
15
|
+
apiBaseUrl: session.api_base_url,
|
|
16
|
+
accessToken: session.access_token,
|
|
17
|
+
slug: args.tenantId,
|
|
18
|
+
dpop: resolveDpopKey(deps.keys, session),
|
|
19
|
+
});
|
|
20
|
+
if (!membership) {
|
|
21
|
+
throw new UsageError(`No tenant membership found for "${args.tenantId}" — check the spelling, or ask a tenant ` +
|
|
22
|
+
'admin to invite you. (Run `rakomi tenants memberships` to see the tenants you already belong to.)');
|
|
23
|
+
}
|
|
24
|
+
deps.tenantConfig.write(membership.id, membership.slug);
|
|
25
|
+
deps.stdout.write(`Active tenant set to ${membership.slug} (${membership.id}) — role: ${membership.role}.\n`);
|
|
10
26
|
}
|