nubase_cli 0.1.7 → 0.1.9

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
@@ -70,8 +70,8 @@ node packages/mcp-bridge/dist/src/index.js
70
70
  "mcpServers": {
71
71
  "nubase": {
72
72
  "type": "stdio",
73
- "command": "node",
74
- "args": ["/absolute/project/path/.nubase/mcp-bridge/dist/src/index.js"],
73
+ "command": "npx",
74
+ "args": ["-y", "nubase_cli@latest"],
75
75
  "env": {
76
76
  "NUBASE_AGENT_ID": "claude-code",
77
77
  "NUBASE_CONFIG": "/absolute/project/path/.nubase/config.json"
@@ -81,6 +81,9 @@ node packages/mcp-bridge/dist/src/index.js
81
81
  }
82
82
  ```
83
83
 
84
+ The bridge code is shared across projects via the npm cache; only `.nubase/config.json`
85
+ (holding this project's `projectKey`) is project-specific, pointed to by `NUBASE_CONFIG`.
86
+
84
87
  You may still set `NUBASE_URL` and `NUBASE_PROJECT_KEY` explicitly. Environment variables take precedence over the saved authorization config.
85
88
 
86
89
  ## Install Agent Skills
@@ -99,15 +102,15 @@ Targets:
99
102
 
100
103
  Use `--skills-scope project` to write `.claude/skills/nubase/**` and `.codex/skills/nubase/**` in the current project instead.
101
104
 
102
- By default, when the target includes `claude`, the command also copies the local MCP bridge to `.nubase/mcp-bridge` and creates or merges project `.mcp.json`:
105
+ By default, when the target includes `claude`, the command creates or merges a project `.mcp.json` that runs the bridge via `npx` (shared across projects through the npm cache):
103
106
 
104
107
  ```json
105
108
  {
106
109
  "mcpServers": {
107
110
  "nubase": {
108
111
  "type": "stdio",
109
- "command": "node",
110
- "args": ["/absolute/project/path/.nubase/mcp-bridge/dist/src/index.js"],
112
+ "command": "npx",
113
+ "args": ["-y", "nubase_cli@<version>"],
111
114
  "env": {
112
115
  "NUBASE_AGENT_ID": "claude-code",
113
116
  "NUBASE_CONFIG": "/absolute/project/path/.nubase/config.json"
@@ -117,8 +120,24 @@ By default, when the target includes `claude`, the command also copies the local
117
120
  }
118
121
  ```
119
122
 
123
+ The `npx` spec is pinned to the installed CLI version for reproducibility. Pass
124
+ `--mcp-delivery local` instead to copy a hermetic, version-pinned bridge into
125
+ `.nubase/mcp-bridge` and reference it with `command: "node"` (no npm dependency at runtime).
126
+
120
127
  Use `--mcp both` to also write project `.codex/config.toml` for Codex. Use `--no-mcp` to skip MCP config.
121
128
 
129
+ ### Permissions
130
+
131
+ The MCP config's `env` block gates what the agent may do. Reads are always allowed; these flags gate write/execute tools:
132
+
133
+ | Flag | Default | Unlocks |
134
+ | --- | --- | --- |
135
+ | `NUBASE_ALLOW_SQL_EXECUTE` | **on** | `sql_execute` (run SQL) |
136
+ | `NUBASE_ALLOW_ADMIN_WRITE` | **on** | create/delete buckets & users, issue/revoke gateway keys |
137
+ | `NUBASE_ALLOW_DANGEROUS_SQL` | **off** | SQL classified DANGEROUS (DROP/TRUNCATE/...) |
138
+
139
+ `install-skills` writes SQL-execute and admin-write into the config by default; dangerous SQL stays off. Opt out per install with `--no-sql-execute` / `--no-admin-write`, or opt into dangerous SQL with `--allow-dangerous-sql`. You can also edit the flags directly in `.mcp.json` (or `.codex/config.toml`) afterwards.
140
+
122
141
  Installed structure:
123
142
 
124
143
  - `nubase/SKILL.md`
package/dist/src/index.js CHANGED
@@ -6,7 +6,7 @@ import { installSkills, parseInstallArgs } from './install-skills.js';
6
6
  import { McpStdioServer } from './mcp-stdio.js';
7
7
  import { NubaseClient } from './nubase-client.js';
8
8
  import { callTool, TOOLS } from './tools.js';
9
- const CLI_VERSION = '0.1.7';
9
+ const CLI_VERSION = '0.1.9';
10
10
  if (process.argv[2] === 'install-skills') {
11
11
  const options = parseInstallArgs(process.argv.slice(3));
12
12
  const installed = await installSkills(options);
@@ -1,6 +1,7 @@
1
1
  export type SkillTarget = 'claude' | 'codex' | 'both';
2
2
  export type SkillInstallScope = 'user' | 'project';
3
3
  export type McpInstallTarget = 'none' | 'claude' | 'codex' | 'both';
4
+ export type McpDelivery = 'npx' | 'local';
4
5
  export interface InstallSkillsOptions {
5
6
  target: SkillTarget;
6
7
  projectDir: string;
@@ -9,6 +10,10 @@ export interface InstallSkillsOptions {
9
10
  skills?: boolean;
10
11
  skillsScope?: SkillInstallScope;
11
12
  mcp?: McpInstallTarget;
13
+ mcpDelivery?: McpDelivery;
14
+ allowSqlExecute?: boolean;
15
+ allowAdminWrite?: boolean;
16
+ allowDangerousSql?: boolean;
12
17
  configPath?: string;
13
18
  homeDir?: string;
14
19
  }
@@ -21,5 +26,9 @@ export declare function parseInstallArgs(argv: string[]): {
21
26
  skills: boolean;
22
27
  skillsScope: SkillInstallScope;
23
28
  mcp: McpInstallTarget;
29
+ mcpDelivery: McpDelivery;
30
+ allowSqlExecute: boolean;
31
+ allowAdminWrite: boolean;
32
+ allowDangerousSql: boolean;
24
33
  configPath: string;
25
34
  };
@@ -19,16 +19,23 @@ export async function installSkills(options) {
19
19
  }
20
20
  }
21
21
  const mcpTargets = resolveMcpTargets(options.mcp ?? 'claude', targets);
22
+ const mcpDelivery = options.mcpDelivery ?? 'npx';
22
23
  let mcpCommand = null;
23
24
  if (mcpTargets.length > 0) {
24
- mcpCommand = await installProjectMcpBridge(options.projectDir);
25
- installed.push(mcpCommand.entrypoint);
25
+ if (mcpDelivery === 'local') {
26
+ mcpCommand = await installProjectMcpBridge(options.projectDir);
27
+ installed.push(mcpCommand.entrypoint);
28
+ }
29
+ else {
30
+ mcpCommand = await npxMcpCommand();
31
+ }
26
32
  }
33
+ const permissionEnv = buildPermissionEnv(options);
27
34
  if (mcpTargets.includes('claude')) {
28
- installed.push(await installClaudeMcpConfig(options.projectDir, configPath, mcpCommand));
35
+ installed.push(await installClaudeMcpConfig(options.projectDir, configPath, mcpCommand, permissionEnv));
29
36
  }
30
37
  if (mcpTargets.includes('codex')) {
31
- installed.push(await installCodexMcpConfig(options.projectDir, configPath, mcpCommand));
38
+ installed.push(await installCodexMcpConfig(options.projectDir, configPath, mcpCommand, permissionEnv));
32
39
  }
33
40
  installed.push(await ensureProjectGitignore(options.projectDir));
34
41
  return installed;
@@ -40,6 +47,10 @@ export function parseInstallArgs(argv) {
40
47
  let skills = true;
41
48
  let skillsScope = 'user';
42
49
  let mcp = 'claude';
50
+ let mcpDelivery = 'npx';
51
+ let allowSqlExecute = true;
52
+ let allowAdminWrite = true;
53
+ let allowDangerousSql = false;
43
54
  let configPath;
44
55
  const authArgs = ['--prompt-only'];
45
56
  for (let i = 0; i < argv.length; i += 1) {
@@ -83,6 +94,22 @@ export function parseInstallArgs(argv) {
83
94
  }
84
95
  skillsScope = value;
85
96
  }
97
+ else if (arg === '--mcp-delivery') {
98
+ const value = argv[++i];
99
+ if (value !== 'npx' && value !== 'local') {
100
+ throw new Error('--mcp-delivery must be npx or local');
101
+ }
102
+ mcpDelivery = value;
103
+ }
104
+ else if (arg === '--no-sql-execute') {
105
+ allowSqlExecute = false;
106
+ }
107
+ else if (arg === '--no-admin-write') {
108
+ allowAdminWrite = false;
109
+ }
110
+ else if (arg === '--allow-dangerous-sql') {
111
+ allowDangerousSql = true;
112
+ }
86
113
  else if (arg === '--config') {
87
114
  const value = argv[++i];
88
115
  if (!value)
@@ -101,7 +128,20 @@ export function parseInstallArgs(argv) {
101
128
  }
102
129
  configPath = configPath ?? projectConfigPath(projectDir);
103
130
  authArgs.push('--config', configPath);
104
- return { target, projectDir, authorize, authArgs, skills, skillsScope, mcp, configPath };
131
+ return {
132
+ target,
133
+ projectDir,
134
+ authorize,
135
+ authArgs,
136
+ skills,
137
+ skillsScope,
138
+ mcp,
139
+ mcpDelivery,
140
+ allowSqlExecute,
141
+ allowAdminWrite,
142
+ allowDangerousSql,
143
+ configPath,
144
+ };
105
145
  }
106
146
  function bundledSkillDir() {
107
147
  return path.join(bundledPackageRoot(), 'skills', 'nubase');
@@ -126,6 +166,20 @@ function resolveMcpTargets(mcp, skillTargets) {
126
166
  const requested = mcp === 'both' ? ['claude', 'codex'] : [mcp];
127
167
  return requested.filter((target) => skillTargets.includes(target));
128
168
  }
169
+ async function npxMcpCommand() {
170
+ const spec = `nubase_cli@${await bundledPackageVersion()}`;
171
+ return { command: 'npx', args: ['-y', spec], entrypoint: spec };
172
+ }
173
+ async function bundledPackageVersion() {
174
+ try {
175
+ const raw = await readFile(path.join(bundledPackageRoot(), 'package.json'), 'utf8');
176
+ const version = JSON.parse(raw).version;
177
+ return typeof version === 'string' && version.trim() ? version.trim() : 'latest';
178
+ }
179
+ catch {
180
+ return 'latest';
181
+ }
182
+ }
129
183
  async function installProjectMcpBridge(projectDir) {
130
184
  const packageRoot = bundledPackageRoot();
131
185
  const destRoot = path.join(projectDir, '.nubase', 'mcp-bridge');
@@ -141,7 +195,18 @@ async function installProjectMcpBridge(projectDir) {
141
195
  entrypoint,
142
196
  };
143
197
  }
144
- async function installClaudeMcpConfig(projectDir, nubaseConfigPath, mcpCommand) {
198
+ // Defaults: SQL execute + admin write ON, dangerous SQL OFF. Reads never need a flag.
199
+ function buildPermissionEnv(options) {
200
+ const env = {};
201
+ if (options.allowSqlExecute ?? true)
202
+ env.NUBASE_ALLOW_SQL_EXECUTE = 'true';
203
+ if (options.allowAdminWrite ?? true)
204
+ env.NUBASE_ALLOW_ADMIN_WRITE = 'true';
205
+ if (options.allowDangerousSql ?? false)
206
+ env.NUBASE_ALLOW_DANGEROUS_SQL = 'true';
207
+ return env;
208
+ }
209
+ async function installClaudeMcpConfig(projectDir, nubaseConfigPath, mcpCommand, permissionEnv) {
145
210
  const mcpConfigPath = path.join(projectDir, '.mcp.json');
146
211
  const config = await readProjectMcpConfig(mcpConfigPath);
147
212
  config.mcpServers = {
@@ -153,17 +218,18 @@ async function installClaudeMcpConfig(projectDir, nubaseConfigPath, mcpCommand)
153
218
  env: {
154
219
  NUBASE_AGENT_ID: 'claude-code',
155
220
  NUBASE_CONFIG: nubaseConfigPath,
221
+ ...permissionEnv,
156
222
  },
157
223
  },
158
224
  };
159
225
  await writeFile(mcpConfigPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
160
226
  return mcpConfigPath;
161
227
  }
162
- async function installCodexMcpConfig(projectDir, nubaseConfigPath, mcpCommand) {
228
+ async function installCodexMcpConfig(projectDir, nubaseConfigPath, mcpCommand, permissionEnv) {
163
229
  const configPath = path.join(projectDir, '.codex', 'config.toml');
164
230
  await mkdir(path.dirname(configPath), { recursive: true });
165
231
  const existing = await readTextIfExists(configPath);
166
- const block = codexMcpBlock(nubaseConfigPath, mcpCommand);
232
+ const block = codexMcpBlock(nubaseConfigPath, mcpCommand, permissionEnv);
167
233
  const next = upsertCodexMcpBlock(existing, block);
168
234
  await writeFile(configPath, next, 'utf8');
169
235
  return configPath;
@@ -188,9 +254,10 @@ async function readTextIfExists(filePath) {
188
254
  throw err;
189
255
  }
190
256
  }
191
- function codexMcpBlock(configPath, mcpCommand) {
257
+ function codexMcpBlock(configPath, mcpCommand, permissionEnv) {
192
258
  const command = mcpCommand?.command ?? 'npx';
193
259
  const args = mcpCommand?.args ?? ['-y', 'nubase_cli@latest'];
260
+ const permissionLines = Object.entries(permissionEnv).map(([key, value]) => `${key} = "${escapeTomlString(value)}"`);
194
261
  return [
195
262
  '[mcp_servers.nubase]',
196
263
  'type = "stdio"',
@@ -201,6 +268,7 @@ function codexMcpBlock(configPath, mcpCommand) {
201
268
  '[mcp_servers.nubase.env]',
202
269
  'NUBASE_AGENT_ID = "codex"',
203
270
  `NUBASE_CONFIG = "${escapeTomlString(configPath)}"`,
271
+ ...permissionLines,
204
272
  '',
205
273
  ].join('\n');
206
274
  }
@@ -47,25 +47,18 @@ export class McpStdioServer {
47
47
  }
48
48
  }
49
49
  readMessage() {
50
- const headerEnd = this.buffer.indexOf('\r\n\r\n');
51
- if (headerEnd === -1)
50
+ // MCP stdio transport frames messages as newline-delimited JSON
51
+ // (one JSON-RPC object per line, no embedded newlines).
52
+ const newlineIndex = this.buffer.indexOf('\n');
53
+ if (newlineIndex === -1)
52
54
  return null;
53
- const header = this.buffer.subarray(0, headerEnd).toString('utf8');
54
- const match = /Content-Length:\s*(\d+)/i.exec(header);
55
- if (!match) {
56
- throw new Error('Missing Content-Length header');
57
- }
58
- const length = Number(match[1]);
59
- const bodyStart = headerEnd + 4;
60
- const bodyEnd = bodyStart + length;
61
- if (this.buffer.length < bodyEnd)
62
- return null;
63
- const body = this.buffer.subarray(bodyStart, bodyEnd).toString('utf8');
64
- this.buffer = this.buffer.subarray(bodyEnd);
65
- return JSON.parse(body);
55
+ const line = this.buffer.subarray(0, newlineIndex).toString('utf8').trim();
56
+ this.buffer = this.buffer.subarray(newlineIndex + 1);
57
+ if (!line)
58
+ return this.readMessage();
59
+ return JSON.parse(line);
66
60
  }
67
61
  write(message) {
68
- const body = JSON.stringify(message);
69
- stdout.write(`Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`);
62
+ stdout.write(`${JSON.stringify(message)}\n`);
70
63
  }
71
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nubase_cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -79,8 +79,8 @@ Expected `.mcp.json` shape:
79
79
  "mcpServers": {
80
80
  "nubase": {
81
81
  "type": "stdio",
82
- "command": "node",
83
- "args": ["/absolute/project/path/.nubase/mcp-bridge/dist/src/index.js"],
82
+ "command": "npx",
83
+ "args": ["-y", "nubase_cli@latest"],
84
84
  "env": {
85
85
  "NUBASE_AGENT_ID": "claude-code",
86
86
  "NUBASE_CONFIG": "/absolute/project/path/.nubase/config.json"