create-rakomi-app 0.1.2 → 0.2.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/README.md CHANGED
@@ -3,6 +3,16 @@
3
3
  Scaffold a [Rakomi](https://rakomi.com) quickstart app in seconds. Rakomi is EU-native
4
4
  authentication as a service.
5
5
 
6
+ ## Ask your agent
7
+
8
+ Add the Rakomi MCP server to your coding agent, then ask it directly:
9
+
10
+ ```sh
11
+ claude mcp add --transport http rakomi https://mcp.rakomi.com/mcp
12
+ ```
13
+
14
+ "List the AI agents connected to my Rakomi tenant."
15
+
6
16
  ## Getting started
7
17
 
8
18
  ```sh
@@ -35,6 +45,8 @@ npm create @rakomi/rakomi-app@latest -- --template nextjs my-app
35
45
  | `--tenant-id <value>` | your tenant id |
36
46
  | `--template-source <url>` | override the archive base (mirror / offline) |
37
47
  | `--yes` | accept defaults, never prompt (non-interactive) |
48
+ | `--connect` | print next steps for connecting an AI agent (the `rakomi` CLI) |
49
+ | `--no-mcp` | skip scaffolding `.mcp.json` / `AGENTS.md` (written by default) |
38
50
  | `-h`, `--help` | show help |
39
51
  | `-V`, `--version` | print the version |
40
52
 
@@ -43,6 +55,11 @@ local `.env` in the new project. Value precedence is: flag > environment variabl
43
55
  default. In a non-interactive context (a pipe, a continuous-integration job, or `--yes`) it never
44
56
  blocks — it uses flags / environment values / defaults and leaves the rest for you to fill in.
45
57
 
58
+ A project-scope `.mcp.json` and an `AGENTS.md` briefing are scaffolded alongside your new app by
59
+ default, so it is agent-ready on first run — open it in Claude Code and run `claude mcp login rakomi`
60
+ to finish sign-in, or point any other AI coding agent at `AGENTS.md` for the same information (the
61
+ MCP server URL, the connect step, and where credentials live). Pass `--no-mcp` to skip both files.
62
+
46
63
  `RAKOMI_REGION` defaults to `eu-central` — a visible data-residency stance, not a mandate. Override
47
64
  it with `--region` or the `RAKOMI_REGION` environment variable for any other region.
48
65
 
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. The CRA support period for each MAJOR is determined in accordance with
13
- **CRA Art. 13(8)** — at least five years, or the product's expected use time where shorter. The authoritative, machine-readable support windows are published at
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,61 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { RAKOMI_SERVER_NAME } from './client-registry.generated.js';
5
+ import { ENV_KEYS } from './env.js';
6
+ import { CLAUDE_CODE_CLIENT, MCP_URL } from './mcp-config.js';
7
+ /** The agent-context filename this module writes, at the project root. */
8
+ export const AGENT_CONTEXT_FILENAME = 'AGENTS.md';
9
+ /**
10
+ * `{{MCP_URL}}` and `{{SERVER_NAME}}` are the registry's own placeholder tokens (`mcp-clients.json`)
11
+ * — substituted here, never baked into the generated module, so one registry entry serves every
12
+ * environment's MCP URL. The scaffolder always writes the default server name, so that is what the
13
+ * rendered instruction names. Mirrors `packages/cli/src/commands/connect.ts`'s helper and contract.
14
+ */
15
+ function withMcpUrl(instruction, mcpUrl) {
16
+ return instruction.replaceAll('{{MCP_URL}}', mcpUrl).replaceAll('{{SERVER_NAME}}', RAKOMI_SERVER_NAME);
17
+ }
18
+ /**
19
+ * Render `AGENTS.md`'s full body for a freshly scaffolded project. Pure and deterministic (no
20
+ * timestamps, no random ids, no directory/template name) so the golden-content test can assert
21
+ * byte-for-byte output regardless of which template or target directory the scaffold used.
22
+ */
23
+ export function agentContextMarkdown() {
24
+ const envKeyList = ENV_KEYS.map((k) => `\`${k}\``).join(', ');
25
+ return ([
26
+ '# Rakomi integration',
27
+ '',
28
+ 'This project is wired to a Rakomi MCP server, so an AI coding agent working in this repository',
29
+ 'can read and manage this tenant on your behalf, once you connect it.',
30
+ '',
31
+ '## MCP server',
32
+ '',
33
+ `- URL: \`${MCP_URL}\``,
34
+ '- Config: `.mcp.json` (already scaffolded in this project root)',
35
+ '',
36
+ '## Connect',
37
+ '',
38
+ withMcpUrl(CLAUDE_CODE_CLIENT.finishInstruction, MCP_URL),
39
+ '',
40
+ '## Credentials',
41
+ '',
42
+ `Tenant credentials live in \`.env\` (${envKeyList}) — never in this file, never in chat, and`,
43
+ 'never committed (`.env` is not tracked by version control). An agent reading this project',
44
+ 'should read values from `.env`, not ask you to paste them.',
45
+ '',
46
+ '## Ask your agent',
47
+ '',
48
+ '> "Connect to my Rakomi tenant using .mcp.json and show me my current users."',
49
+ '',
50
+ ].join('\n') + '\n');
51
+ }
52
+ /**
53
+ * Write `AGENTS.md` into the freshly scaffolded project root. Always writes unconditionally when
54
+ * called (gated by the same `--no-mcp` opt-out as `.mcp.json` at the call site, `index.ts`) — a
55
+ * fresh quickstart template never ships its own `AGENTS.md` (verified by
56
+ * `test/agent-context.test.ts`'s manifest scan), so there is nothing to merge and nothing to
57
+ * clobber. LF-terminated, the same convention every other writer in this package uses.
58
+ */
59
+ export async function writeAgentContext(targetDir) {
60
+ await writeFile(join(targetDir, AGENT_CONTEXT_FILENAME), agentContextMarkdown(), 'utf8');
61
+ }
@@ -0,0 +1,157 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export const RAKOMI_SERVER_NAME = "rakomi";
3
+ export const CLIENT_REGISTRY = [
4
+ {
5
+ id: "claude-code",
6
+ displayName: "Claude Code",
7
+ tier: "written",
8
+ status: "verified",
9
+ 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).",
10
+ config: {
11
+ scope: "project",
12
+ pathTemplate: ".mcp.json",
13
+ serialization: "json",
14
+ topLevelKey: "mcpServers",
15
+ remoteUrlField: "url",
16
+ extraFields: { "type": "http" },
17
+ },
18
+ evidenceUrl: "https://code.claude.com/docs/en/mcp",
19
+ },
20
+ {
21
+ id: "vscode",
22
+ displayName: "VS Code",
23
+ tier: "written",
24
+ status: "documented-shape-only",
25
+ 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.",
26
+ config: {
27
+ scope: "project",
28
+ pathTemplate: ".vscode/mcp.json",
29
+ serialization: "json",
30
+ topLevelKey: "servers",
31
+ remoteUrlField: "url",
32
+ extraFields: { "type": "http" },
33
+ },
34
+ evidenceUrl: "https://code.visualstudio.com/docs/agents/reference/mcp-configuration",
35
+ },
36
+ {
37
+ id: "cursor",
38
+ displayName: "Cursor",
39
+ tier: "written",
40
+ status: "documented-shape-only",
41
+ 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).",
42
+ config: {
43
+ scope: "project",
44
+ pathTemplate: ".cursor/mcp.json",
45
+ serialization: "json",
46
+ topLevelKey: "mcpServers",
47
+ remoteUrlField: "url",
48
+ extraFields: {},
49
+ },
50
+ evidenceUrl: "https://cursor.com/docs/mcp",
51
+ },
52
+ {
53
+ id: "antigravity",
54
+ displayName: "Google Antigravity",
55
+ tier: "written",
56
+ status: "documented-shape-only",
57
+ finishInstruction: "Open the MCP Store or your MCP settings in Antigravity — it handles OAuth automatically for servers that support dynamic client registration (DCR).",
58
+ config: {
59
+ scope: "project",
60
+ pathTemplate: ".agents/mcp_config.json",
61
+ serialization: "json",
62
+ topLevelKey: "mcpServers",
63
+ remoteUrlField: "serverUrl",
64
+ extraFields: {},
65
+ },
66
+ evidenceUrl: "https://antigravity.google/docs/ide/mcp/",
67
+ },
68
+ {
69
+ id: "gemini-cli",
70
+ displayName: "Gemini CLI",
71
+ tier: "written",
72
+ status: "documented-shape-only",
73
+ 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.",
74
+ config: {
75
+ scope: "project",
76
+ pathTemplate: ".gemini/settings.json",
77
+ serialization: "json",
78
+ topLevelKey: "mcpServers",
79
+ remoteUrlField: "httpUrl",
80
+ extraFields: {},
81
+ },
82
+ evidenceUrl: "https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html",
83
+ },
84
+ {
85
+ id: "zed",
86
+ displayName: "Zed",
87
+ tier: "written",
88
+ status: "documented-shape-only",
89
+ 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.",
90
+ config: {
91
+ scope: "project",
92
+ pathTemplate: ".zed/settings.json",
93
+ serialization: "json",
94
+ topLevelKey: "context_servers",
95
+ remoteUrlField: "url",
96
+ extraFields: {},
97
+ },
98
+ evidenceUrl: "https://zed.dev/docs/ai/mcp",
99
+ },
100
+ {
101
+ id: "codex-cli",
102
+ displayName: "OpenAI Codex CLI",
103
+ tier: "written",
104
+ status: "documented-shape-only",
105
+ finishInstruction: "Run `codex mcp login {{SERVER_NAME}}` to finish sign-in in your browser.",
106
+ config: {
107
+ scope: "user",
108
+ pathTemplate: ".codex/config.toml",
109
+ serialization: "toml",
110
+ topLevelKey: "mcp_servers",
111
+ remoteUrlField: "url",
112
+ extraFields: {},
113
+ },
114
+ evidenceUrl: "https://learn.chatgpt.com/docs/extend/mcp?surface=cli",
115
+ },
116
+ {
117
+ id: "devin-desktop",
118
+ displayName: "Devin Desktop",
119
+ tier: "written",
120
+ status: "documented-shape-only",
121
+ finishInstruction: "Open Devin Desktop — it supports OAuth for each transport type and prompts you to connect the first time.",
122
+ config: {
123
+ scope: "user",
124
+ pathTemplate: ".codeium/windsurf/mcp_config.json",
125
+ serialization: "json",
126
+ topLevelKey: "mcpServers",
127
+ remoteUrlField: "serverUrl",
128
+ extraFields: {},
129
+ },
130
+ evidenceUrl: "https://docs.devin.ai/desktop/cascade/mcp",
131
+ },
132
+ {
133
+ id: "claude-desktop",
134
+ displayName: "Claude Desktop",
135
+ tier: "instructed",
136
+ status: "verified",
137
+ 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.",
138
+ evidenceUrl: "https://support.claude.com/en/articles/11175166-about-custom-connectors-remote-mcp",
139
+ },
140
+ {
141
+ id: "chatgpt",
142
+ displayName: "ChatGPT",
143
+ tier: "instructed",
144
+ status: "documented-shape-only",
145
+ 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.",
146
+ evidenceUrl: "https://developers.openai.com/api/docs/mcp",
147
+ },
148
+ {
149
+ id: "jetbrains",
150
+ displayName: "JetBrains AI Assistant",
151
+ tier: "instructed",
152
+ status: "documented-shape-only",
153
+ 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.",
154
+ evidenceUrl: "https://www.jetbrains.com/help/ai-assistant/mcp.html",
155
+ },
156
+ ];
157
+ export const KNOWN_CLIENTS = CLIENT_REGISTRY.map((c) => c.id);
package/dist/env.js CHANGED
@@ -1,12 +1,86 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import { writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- /** Keys the scaffolder collects, in written order. Names follow `RAKOMI_[A-Z0-9_]+`. */
5
- export const ENV_KEYS = ['RAKOMI_REGION', 'RAKOMI_TENANT_ID', 'RAKOMI_API_KEY'];
4
+ /**
5
+ * Keys the scaffolder collects, in written order. Names follow `RAKOMI_[A-Z0-9_]+`.
6
+ * `RAKOMI_CLIENT_SECRET`/`RAKOMI_REDIRECT_URI` are collected ONLY for the `node` template — see
7
+ * `NODE_ONLY_KEYS` below; every other template's dev server never reads either.
8
+ */
9
+ export const ENV_KEYS = [
10
+ 'RAKOMI_REGION',
11
+ 'RAKOMI_TENANT_ID',
12
+ 'RAKOMI_API_KEY',
13
+ 'RAKOMI_CLIENT_ID',
14
+ 'RAKOMI_CLIENT_SECRET',
15
+ 'RAKOMI_REDIRECT_URI',
16
+ 'RAKOMI_ISSUER',
17
+ ];
6
18
  /** The default EU region — a visible, overridable data-residency stance, not a mandate. */
7
19
  export const DEFAULT_REGION = 'eu-central';
8
- /** Keys whose value is a credential and must never be echoed to stdout / logs / summaries. */
9
- export const SECRET_KEYS = new Set(['RAKOMI_API_KEY']);
20
+ /** The node quickstart's own `.env.example` default — an OAuth client registered for local dev
21
+ * typically has this exact redirect URI, so it doubles as a working starting point. */
22
+ export const DEFAULT_NODE_REDIRECT_URI = 'http://localhost:3000/callback';
23
+ /**
24
+ * Keys whose value is a credential and must never be echoed to stdout / logs / summaries.
25
+ * `RAKOMI_CLIENT_ID` is deliberately absent — an OAuth client_id is a PUBLIC, publishable
26
+ * identifier (PKCE public clients ship it in bundled JS), not a secret.
27
+ */
28
+ export const SECRET_KEYS = new Set(['RAKOMI_API_KEY', 'RAKOMI_CLIENT_SECRET']);
29
+ /**
30
+ * The `node` quickstart is the only template whose OWN server performs a confidential-client
31
+ * OAuth code exchange (`examples/quickstarts/node/src/config.ts`) — `RAKOMI_REDIRECT_URI` is
32
+ * REQUIRED there (`loadConfig()` throws a `ConfigError` at boot without it: the scaffolder used
33
+ * to omit it entirely, so a freshly-scaffolded node app never started) and `RAKOMI_CLIENT_SECRET`
34
+ * is read for the confidential-client case node's own README instructs the user to set up
35
+ * (optional — an unset value degrades to a public/PKCE-only client, per that file's own comment).
36
+ * Every browser-based template (nextjs/react/expo) drives its OAuth flow through `@rakomi/react`
37
+ * client-side and never reads either var — collecting/writing them there would be a meaningless
38
+ * prompt and a dead `.env` line for those templates.
39
+ */
40
+ const NODE_ONLY_KEYS = new Set(['RAKOMI_CLIENT_SECRET', 'RAKOMI_REDIRECT_URI']);
41
+ /**
42
+ * `RAKOMI_ISSUER` — the issuer of the environment whose tokens the app accepts (every environment
43
+ * is its own issuer with its own signing keys). Read, and required at boot / per request, ONLY by
44
+ * the templates whose own server verifies access tokens: `node` (`src/config.ts`) and `nextjs`
45
+ * (`src/lib/server/config.ts`). The browser/mobile templates never verify a token themselves.
46
+ */
47
+ const ISSUER_TEMPLATES = new Set(['node', 'nextjs']);
48
+ /** The keys actually relevant to a given template — `ENV_KEYS` minus the `node`-only keys for
49
+ * every other template (or an absent/unknown slug — the conservative default), unchanged (full
50
+ * set) for `node` itself. Drives both what the wizard prompts for (`prompt.ts`'s `collectEnv`)
51
+ * and what `renderDotenv` writes. */
52
+ export function keysForTemplate(templateSlug) {
53
+ if (templateSlug === 'node')
54
+ return ENV_KEYS;
55
+ return ENV_KEYS.filter((key) => !NODE_ONLY_KEYS.has(key) &&
56
+ (key !== 'RAKOMI_ISSUER' || (templateSlug !== undefined && ISSUER_TEMPLATES.has(templateSlug))));
57
+ }
58
+ /**
59
+ * Every scaffolded template reads `RAKOMI_REGION` / `RAKOMI_TENANT_ID` / `RAKOMI_API_KEY` under
60
+ * their canonical `RAKOMI_*` name — but each template's OWN `.env.example` names its OAuth
61
+ * `client_id` differently, following that framework's inline-at-build-time convention:
62
+ * Next.js `NEXT_PUBLIC_*`, Vite `VITE_*`, Expo `EXPO_PUBLIC_*`; a server-only Node app keeps the
63
+ * plain `RAKOMI_*` form. Writing the collected value under the WRONG name means the app never
64
+ * reads it and fails with a first-run "missing client" error despite a fully-filled `.env`
65
+ * (the incident this mapping exists to close). See `examples/quickstarts/{slug}/.env.example`
66
+ * for the source of truth this table mirrors.
67
+ */
68
+ const CLIENT_ID_KEY_BY_TEMPLATE = {
69
+ nextjs: 'NEXT_PUBLIC_RAKOMI_CLIENT_ID',
70
+ react: 'VITE_RAKOMI_CLIENT_ID',
71
+ expo: 'EXPO_PUBLIC_RAKOMI_CLIENT_ID',
72
+ node: 'RAKOMI_CLIENT_ID',
73
+ };
74
+ /**
75
+ * Resolve the `.env` key NAME a given canonical `EnvKey` must be written under for a specific
76
+ * template. Only `RAKOMI_CLIENT_ID` varies by template today; every other key keeps its
77
+ * canonical `RAKOMI_*` name across every template.
78
+ */
79
+ export function envKeyNameForTemplate(templateSlug, key) {
80
+ if (key !== 'RAKOMI_CLIENT_ID')
81
+ return key;
82
+ return CLIENT_ID_KEY_BY_TEMPLATE[templateSlug] ?? key;
83
+ }
10
84
  const CONTROL_CHARS = /[\u0000-\u001F\u007F]/g;
11
85
  /**
12
86
  * Serialise a single `KEY=value` line in canonical dotenv form:
@@ -23,15 +97,18 @@ export function dotenvLine(key, rawValue) {
23
97
  return `${key}="${escaped}"`;
24
98
  }
25
99
  /**
26
- * Render a full `.env` body from collected values. Always LF-terminated, one key per line,
27
- * in `ENV_KEYS` order. A missing value is written as an empty assignment so the file lists
28
- * every key for the user to complete.
100
+ * Render a full `.env` body from collected values. Always LF-terminated, one key per line, in
101
+ * `ENV_KEYS` order restricted to `keysForTemplate(templateSlug)` — a key the given template never
102
+ * reads is never written at all, rather than left as a dead empty assignment. A missing value for
103
+ * a key the template DOES read is still written as an empty assignment so the file lists it for
104
+ * the user to complete. Each key is written under the NAME the given template actually reads
105
+ * (`envKeyNameForTemplate`) — not necessarily its canonical `RAKOMI_*` form.
29
106
  */
30
- export function renderDotenv(values) {
31
- const lines = ENV_KEYS.map((key) => dotenvLine(key, values[key] ?? ''));
107
+ export function renderDotenv(values, templateSlug) {
108
+ const lines = keysForTemplate(templateSlug).map((key) => dotenvLine(envKeyNameForTemplate(templateSlug, key), values[key] ?? ''));
32
109
  return lines.join('\n') + '\n';
33
110
  }
34
111
  /** Write the `.env` file into the target project directory. */
35
- export async function writeEnvFile(targetDir, values) {
36
- await writeFile(join(targetDir, '.env'), renderDotenv(values), 'utf8');
112
+ export async function writeEnvFile(targetDir, values, templateSlug) {
113
+ await writeFile(join(targetDir, '.env'), renderDotenv(values, templateSlug), 'utf8');
37
114
  }
package/dist/index.js CHANGED
@@ -6,8 +6,10 @@ import { isAbsolute, join, relative, resolve } from 'node:path';
6
6
  import process from 'node:process';
7
7
  import { pathToFileURL } from 'node:url';
8
8
  import { parseArgs } from 'node:util';
9
+ import { writeAgentContext } from './agent-context.js';
9
10
  import { writeEnvFile } from './env.js';
10
11
  import { CliError, EXIT, UsageError } from './errors.js';
12
+ import { writeMcpConfig } from './mcp-config.js';
11
13
  import { collectEnv, createTtyAsk } from './prompt.js';
12
14
  import { assertTargetWritable, GithubCodeloadSource, materializeArchive } from './source.js';
13
15
  import { findTemplate, slugList } from './templates.js';
@@ -45,9 +47,11 @@ async function dispatch(args, deps) {
45
47
  template: { type: 'string' },
46
48
  region: { type: 'string' },
47
49
  'tenant-id': { type: 'string' },
50
+ 'client-id': { type: 'string' },
48
51
  'template-source': { type: 'string' },
49
52
  yes: { type: 'boolean' },
50
53
  connect: { type: 'boolean' },
54
+ 'no-mcp': { type: 'boolean' },
51
55
  help: { type: 'boolean', short: 'h' },
52
56
  version: { type: 'boolean', short: 'V' },
53
57
  },
@@ -82,15 +86,22 @@ async function dispatch(args, deps) {
82
86
  flags.RAKOMI_REGION = values.region;
83
87
  if (typeof values['tenant-id'] === 'string')
84
88
  flags.RAKOMI_TENANT_ID = values['tenant-id'];
89
+ if (typeof values['client-id'] === 'string')
90
+ flags.RAKOMI_CLIENT_ID = values['client-id'];
85
91
  const interactive = deps.isTTY && values.yes !== true && !deps.env.CI;
86
- const envValues = await collectEnv({ flags, env: deps.env, interactive, ask: deps.ask });
92
+ const envValues = await collectEnv({ flags, env: deps.env, interactive, ask: deps.ask }, template.slug);
87
93
  const source = deps.source ?? makeDefaultSource(values, deps.env);
88
94
  const archive = await source.fetchArchive(template);
89
95
  await materializeArchive(archive, targetDir);
90
96
  await ensureEnvIgnored(targetDir);
91
- await writeEnvFile(targetDir, envValues);
97
+ await writeEnvFile(targetDir, envValues, template.slug);
98
+ const mcpConfigWritten = values['no-mcp'] !== true;
99
+ if (mcpConfigWritten) {
100
+ await writeMcpConfig(targetDir);
101
+ await writeAgentContext(targetDir);
102
+ }
92
103
  const pm = detectPackageManager(deps.env.npm_config_user_agent);
93
- deps.stdout.write(postInstallMessage(template.slug, rawTarget, pm));
104
+ deps.stdout.write(postInstallMessage(template.slug, rawTarget, pm, mcpConfigWritten));
94
105
  if (values.connect === true) {
95
106
  deps.stdout.write(connectInstructions());
96
107
  }
@@ -0,0 +1,48 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import { writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { CLIENT_REGISTRY } from './client-registry.generated.js';
5
+ import { CliError, EXIT } from './errors.js';
6
+ /**
7
+ * The Rakomi MCP resource server's canonical URL. Byte-frozen: `https` scheme, lowercase host,
8
+ * no port, the `/mcp` Streamable-HTTP mount path. Never hand-type this literal a second time —
9
+ * import this constant.
10
+ */
11
+ export const MCP_URL = 'https://mcp.rakomi.com/mcp';
12
+ function mustFindClient(id) {
13
+ const entry = CLIENT_REGISTRY.find((c) => c.id === id);
14
+ if (!entry) {
15
+ throw new CliError(`client-registry.generated.ts is missing the "${id}" entry — run \`node scripts/gen-mcp-clients.mjs\` and commit the result.`, EXIT.FAIL);
16
+ }
17
+ return entry;
18
+ }
19
+ /**
20
+ * Claude Code's registry entry — the single source `mcpConfigJson()` and `agent-context.ts` derive
21
+ * the `.mcp.json` shape and the connect `finishInstruction` from. Never hand-duplicate either;
22
+ * import from here.
23
+ */
24
+ export const CLAUDE_CODE_CLIENT = mustFindClient('claude-code');
25
+ /**
26
+ * The exact content written to `.mcp.json` — matches the shape `claude mcp add --transport http`
27
+ * produces (topLevelKey `mcpServers`, remoteUrlField `url`, extraFields `{ type: "http" }`),
28
+ * DERIVED from `CLAUDE_CODE_CLIENT.config` rather than a second hand-typed literal. No
29
+ * `client_id` — CIMD discovery needs none.
30
+ */
31
+ export function mcpConfigJson() {
32
+ const config = CLAUDE_CODE_CLIENT.config;
33
+ if (!config) {
34
+ throw new CliError('The claude-code registry entry carries no config block.', EXIT.FAIL);
35
+ }
36
+ const rakomiEntry = { ...config.extraFields, [config.remoteUrlField]: MCP_URL };
37
+ return { [config.topLevelKey]: { rakomi: rakomiEntry } };
38
+ }
39
+ /**
40
+ * Write `.mcp.json` into the freshly scaffolded project root. Always writes unconditionally — a
41
+ * fresh quickstart template never ships its own `.mcp.json` (verified by
42
+ * `test/mcp-config.test.ts`'s manifest scan), so there is nothing to merge and nothing to clobber.
43
+ * Pretty-printed, LF-terminated, the same convention every other writer in this package uses
44
+ * (`env.ts`'s `writeEnvFile`).
45
+ */
46
+ export async function writeMcpConfig(targetDir) {
47
+ await writeFile(join(targetDir, '.mcp.json'), JSON.stringify(mcpConfigJson(), null, 2) + '\n', 'utf8');
48
+ }
package/dist/prompt.js CHANGED
@@ -1,21 +1,45 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  import { stdin, stdout } from 'node:process';
3
3
  import { createInterface } from 'node:readline/promises';
4
- import { DEFAULT_REGION, ENV_KEYS, SECRET_KEYS } from './env.js';
4
+ import { DEFAULT_NODE_REDIRECT_URI, DEFAULT_REGION, ENV_KEYS, keysForTemplate, SECRET_KEYS } from './env.js';
5
5
  export const FIELDS = [
6
6
  { key: 'RAKOMI_REGION', label: 'Data region', defaultValue: DEFAULT_REGION },
7
7
  { key: 'RAKOMI_TENANT_ID', label: 'Tenant ID' },
8
8
  { key: 'RAKOMI_API_KEY', label: 'API key' },
9
+ { key: 'RAKOMI_CLIENT_ID', label: 'OAuth Client ID' },
10
+ { key: 'RAKOMI_CLIENT_SECRET', label: 'OAuth Client Secret (leave blank for a public/PKCE-only client)' },
11
+ { key: 'RAKOMI_REDIRECT_URI', label: 'Redirect URI', defaultValue: DEFAULT_NODE_REDIRECT_URI },
12
+ { key: 'RAKOMI_ISSUER', label: 'Issuer of your environment' },
9
13
  ];
14
+ /**
15
+ * Extra guidance appended to a field's prompt line — where to GET a value the wizard cannot
16
+ * derive on its own, or a precondition the value itself carries. `RAKOMI_CLIENT_ID`: every tenant
17
+ * signup auto-provisions a default OAuth client at creation time, so the value already exists —
18
+ * find it rather than create it. `RAKOMI_API_KEY`: a signup's LIVE key (`akm_live_`) does not
19
+ * authenticate requests until the account owner verifies their e-mail — paste the TEST key
20
+ * (`akm_test_`) here to get this app running immediately, then swap in the live key once verified.
21
+ */
22
+ const FIELD_HINTS = {
23
+ RAKOMI_API_KEY: 'paste the test key (akm_test_...) to start now — the live key needs email verification first',
24
+ RAKOMI_CLIENT_ID: 'from your Rakomi dashboard -> Settings -> Development credentials',
25
+ RAKOMI_ISSUER: 'the iss of your environment\'s tokens, e.g. https://api.rakomi.com/t/tn_.../test',
26
+ };
10
27
  /**
11
28
  * Resolve all env values by precedence: explicit flag > `RAKOMI_*` env var > interactive
12
29
  * prompt > documented default. Never prompts in non-interactive mode (so a CI pipe never
13
30
  * hangs); there it falls back to env/flag/default, leaving the rest empty for the user.
14
- * Secret values are never echoed back.
31
+ * Secret values are never echoed back. `templateSlug` restricts which fields are even asked
32
+ * about — a key the given template never reads (see `keysForTemplate`) is skipped entirely,
33
+ * never prompted for and never present in the returned object; optional so every pre-existing
34
+ * caller/fixture (which meant "every canonical key applies") keeps compiling and behaving
35
+ * unchanged.
15
36
  */
16
- export async function collectEnv(deps) {
37
+ export async function collectEnv(deps, templateSlug) {
38
+ const relevant = new Set(keysForTemplate(templateSlug));
17
39
  const out = {};
18
40
  for (const field of FIELDS) {
41
+ if (!relevant.has(field.key))
42
+ continue;
19
43
  const fromFlag = deps.flags[field.key];
20
44
  if (fromFlag !== undefined && fromFlag !== '') {
21
45
  out[field.key] = fromFlag;
@@ -38,8 +62,9 @@ export async function collectEnv(deps) {
38
62
  }
39
63
  function promptText(field) {
40
64
  const secret = SECRET_KEYS.has(field.key) ? ' (kept local, never sent anywhere)' : '';
65
+ const hint = FIELD_HINTS[field.key] ? ` (${FIELD_HINTS[field.key]})` : '';
41
66
  const dflt = field.defaultValue !== undefined ? ` [${field.defaultValue}]` : '';
42
- return `${field.label}${secret}${dflt}: `;
67
+ return `${field.label}${secret}${hint}${dflt}: `;
43
68
  }
44
69
  /** A real-TTY prompt backed by `node:readline/promises`. */
45
70
  export function createTtyAsk() {
package/dist/usage.js CHANGED
@@ -36,16 +36,22 @@ export function helpText() {
36
36
  ' --template <slug> which quickstart to scaffold (required)',
37
37
  ' --region <value> data region (default: eu-central)',
38
38
  ' --tenant-id <value> your tenant id',
39
+ ' --client-id <value> your OAuth client_id (dashboard -> Integration -> Applications)',
39
40
  ' --template-source <url> override the archive base (mirror / offline)',
40
41
  ' --yes accept defaults, never prompt (non-interactive)',
41
42
  ' --connect print next steps for connecting an AI agent (the rakomi CLI)',
43
+ " --no-mcp don't scaffold .mcp.json / AGENTS.md (written by default — agent-ready on first run)",
42
44
  ' -h, --help show this help and exit',
43
45
  ' -V, --version print the version and exit',
44
46
  '',
45
47
  'Environment variables collected into the new project\'s .env:',
46
- ' RAKOMI_REGION, RAKOMI_TENANT_ID, RAKOMI_API_KEY',
48
+ ' RAKOMI_REGION, RAKOMI_TENANT_ID, RAKOMI_API_KEY, RAKOMI_CLIENT_ID',
49
+ ' RAKOMI_ISSUER (node, nextjs: the issuer of your environment, e.g. https://api.rakomi.com/t/tn_...)',
47
50
  ' (RAKOMI_API_KEY is read from the prompt or the environment, never a flag,',
48
- ' and is written only to your local .env — never transmitted.)',
51
+ ' and is written only to your local .env — never transmitted. RAKOMI_CLIENT_ID is',
52
+ " written under the target template's own convention, e.g. NEXT_PUBLIC_RAKOMI_CLIENT_ID.",
53
+ ' Your tenant signup auto-provisions a default OAuth client for you — find its Client ID in',
54
+ ' your Rakomi dashboard: Settings -> Development credentials.)',
49
55
  '',
50
56
  `After scaffolding, the next-step walkthrough lives at ${PORTAL_HOST}/quickstart/<slug>.`,
51
57
  '',
@@ -60,18 +66,26 @@ export function usageLine() {
60
66
  * manager-aware command list. The install step is framed as the user's own next step that
61
67
  * depends on the public npm registry, not a guarantee. No color is emitted (NO_COLOR-safe by
62
68
  * construction) and the copy is stack-neutral so a tutorial can quote it verbatim.
69
+ *
70
+ * `mcpConfigWritten` (default `true`) reports whether `.mcp.json` + `AGENTS.md` were scaffolded
71
+ * into the new project (they are, unless `--no-mcp` was passed) — when they were, one extra line
72
+ * tells the user the project is already agent-ready and what to run to finish sign-in, plus a
73
+ * pointer to AGENTS.md for a fuller briefing.
63
74
  */
64
- export function postInstallMessage(slug, directory, pm) {
65
- return [
75
+ export function postInstallMessage(slug, directory, pm, mcpConfigWritten = true) {
76
+ const lines = [
66
77
  '',
67
78
  `Done. Your Rakomi ${slug} app is ready in ${directory}`,
68
79
  '',
69
80
  'Next steps:',
70
81
  ` 1. cd ${directory}`,
71
- ` 2. ${installCommand(pm)} (installs @rakomi/node from the public npm registry)`,
82
+ ` 2. ${installCommand(pm)} (installs the Rakomi SDK from the public npm registry)`,
72
83
  ` 3. ${runCommand(pm)}`,
73
84
  '',
74
- `Walkthrough: ${portalUrl(slug)}`,
75
- '',
76
- ].join('\n');
85
+ ];
86
+ if (mcpConfigWritten) {
87
+ lines.push('Agent-ready: .mcp.json points Claude Code at the Rakomi MCP server — run `claude mcp login rakomi` to finish sign-in.', 'See AGENTS.md for the full briefing any AI agent working in this project can read.', '');
88
+ }
89
+ lines.push(`Walkthrough: ${portalUrl(slug)}`, '');
90
+ return lines.join('\n');
77
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-rakomi-app",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffold a Rakomi quickstart app. EU-native auth-as-a-service. npx create-rakomi-app --template <slug>",
5
5
  "keywords": [
6
6
  "rakomi",
@@ -43,6 +43,6 @@
43
43
  "build": "tsc",
44
44
  "typecheck": "tsc --noEmit",
45
45
  "lint": "eslint src/ test/ --max-warnings=0",
46
- "test": "vitest run"
46
+ "test": "node ../../scripts/assert-not-story-agent-worktree.mjs test && node ../../scripts/ci/vitest-fork-crash-retry.mjs --results ./test-results.json -- vitest run"
47
47
  }
48
48
  }
package/sbom.cdx.json CHANGED
@@ -26,10 +26,10 @@
26
26
  },
27
27
  "component": {
28
28
  "type": "library",
29
- "bom-ref": "pkg:npm/create-rakomi-app@0.1.2",
29
+ "bom-ref": "pkg:npm/create-rakomi-app@0.2.0",
30
30
  "name": "create-rakomi-app",
31
- "version": "0.1.2",
32
- "purl": "pkg:npm/create-rakomi-app@0.1.2",
31
+ "version": "0.2.0",
32
+ "purl": "pkg:npm/create-rakomi-app@0.2.0",
33
33
  "licenses": [
34
34
  {
35
35
  "license": {