@cef-ai/cli 0.2.2 → 0.2.4

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
@@ -82,7 +82,10 @@ cef dev
82
82
  # Start with a specific config
83
83
  cef dev --config ./my-agent/cef.config.yaml
84
84
 
85
- # Deploy to platform (requires .env with auth tokens)
85
+ # Authenticate (opens browser)
86
+ cef login
87
+
88
+ # Deploy to platform
86
89
  cef deploy --config ./cef.config.yaml
87
90
  ```
88
91
 
@@ -116,7 +119,7 @@ The recommended flow from idea to production:
116
119
  6. Iterate Edit agent code; changes hot-reload automatically
117
120
  .cef-dev/ is cleared on shutdown (use --persist to keep)
118
121
 
119
- 7. Deploy Create .env with CEF_AUTH_TOKEN, CEF_ORCHESTRATOR_URL
122
+ 7. Deploy cef login (browser-based auth, one-time)
120
123
  cef deploy
121
124
  IDs are written back into cef.config.yaml
122
125
 
@@ -130,6 +133,9 @@ The recommended flow from idea to production:
130
133
 
131
134
  | Command | Description | Auth Required |
132
135
  |-|-|-|
136
+ | `cef login` | Authenticate via browser (saves token to `~/.cef/credentials.json`) | No |
137
+ | `cef logout` | Clear stored credentials | No |
138
+ | `cef whoami` | Show current authentication status | No |
133
139
  | `cef dev` | Start local dev server with UI, hot-reload, real SQLite cubbies | No |
134
140
  | `cef deploy` | Deploy topology to orchestrator from cef.config.yaml | Yes |
135
141
  | `cef deploy status` | Compare local config vs deployed state | Yes |
@@ -470,9 +476,9 @@ cef service list --config ./app/cef.config.yaml
470
476
 
471
477
  | Option | Default | Description |
472
478
  |-|-|-|
473
- | `--config <path>` | `./cef.config.yaml` | Path to config file (finds `.env` in same dir) |
479
+ | `--config <path>` | `./cef.config.yaml` | Path to config file |
474
480
 
475
- Requires `CEF_ROB_API_URL` in `.env`.
481
+ Requires `CEF_ROB_API_URL` (defaults to stage URL if not set).
476
482
 
477
483
  Output:
478
484
 
@@ -506,7 +512,7 @@ cef service create --name my-agent --bucket-id 1505
506
512
  | `--config <path>` | `./cef.config.yaml` | No | Path to config file |
507
513
  | `--bucket-id <id>` | `0` | No | DDC bucket ID |
508
514
 
509
- Requires `CEF_ROB_API_URL` in `.env`.
515
+ Requires `CEF_ROB_API_URL` (defaults to stage URL if not set).
510
516
 
511
517
  Returns the service name, `agentServiceId`, and `agentServicePubKey`. Use the pubkey in your `cef.config.yaml`.
512
518
 
@@ -1001,26 +1007,27 @@ In local dev mode, raft queries throw `"not available in local dev mode"`. In de
1001
1007
 
1002
1008
  ---
1003
1009
 
1004
- ## Environment Variables
1010
+ ## Authentication
1005
1011
 
1006
- Create a `.env` file in the same directory as your `cef.config.yaml`:
1012
+ Run `cef login` to authenticate via browser. Credentials are saved to `~/.cef/credentials.json` and used automatically by all commands that require auth.
1007
1013
 
1008
1014
  ```bash
1009
- CEF_AUTH_TOKEN=your-jwt-token-here
1010
- CEF_ORCHESTRATOR_URL=https://orchestrator.compute.test.ddcdragon.com
1011
- CEF_ROB_API_URL=https://rob.compute.test.ddcdragon.com/rms-node-backend
1015
+ cef login # Opens browser, saves token
1016
+ cef whoami # Check current auth status
1017
+ cef logout # Clear stored credentials
1012
1018
  ```
1013
1019
 
1014
- | Variable | Required For | Description |
1020
+ No `.env` file is needed. All API URLs default to stage:
1021
+
1022
+ | Variable | Default | Description |
1015
1023
  |-|-|-|
1016
- | `CEF_AUTH_TOKEN` | deploy, status, clone, delete, service | JWT bearer token (same as ROB UI) |
1017
- | `CEF_ORCHESTRATOR_URL` | deploy, status, clone, delete | Orchestrator base URL |
1018
- | `CEF_ROB_API_URL` | service list/create, workspace registration | ROB API base URL |
1019
- | `CEF_ROB_ORIGIN` | (optional) | Override Origin/Referer header for ROB requests |
1024
+ | `CEF_ORCHESTRATOR_URL` | `https://orchestrator.compute.test.ddcdragon.com` | Orchestrator base URL |
1025
+ | `CEF_ROB_API_URL` | `https://rob-api.compute.test.ddcdragon.com/rms-node-backend` | ROB API base URL |
1026
+ | `CEF_ROB_ORIGIN` | (derived from ROB API URL) | Override Origin/Referer header |
1020
1027
 
1021
- `cef dev` does **not** require any environment variables.
1028
+ To override URLs, set them as environment variables or in a `.env` file next to your `cef.config.yaml`.
1022
1029
 
1023
- Copy from `example/.env.example` and fill in your credentials.
1030
+ `cef dev` does **not** require authentication or any environment variables.
1024
1031
 
1025
1032
  ---
1026
1033
 
@@ -1239,17 +1246,17 @@ npm run build # Build TypeScript
1239
1246
  | Suite | When to use | Auth needed | Speed |
1240
1247
  |-|-|-|-|
1241
1248
  | Unit (`npm test`) | Always; validates logic without external deps | No | Fast |
1242
- | Integration | Verify API client against dev environment | Yes (`.env`) | Medium |
1243
- | E2E | Full lifecycle: deploy, test, delete | Yes (`.env` + `CEF_E2E_AGENT_SERVICE_PUB_KEY`) | Slow (120s timeout) |
1249
+ | Integration | Verify API client against dev environment | Yes (`cef login`) | Medium |
1250
+ | E2E | Full lifecycle: deploy, test, delete | Yes (`cef login` + `CEF_E2E_AGENT_SERVICE_PUB_KEY`) | Slow (120s timeout) |
1244
1251
 
1245
- For integration/E2E: copy `example/.env.example` to `.env` and set credentials.
1252
+ For integration/E2E: run `cef login` first, then set `CEF_E2E_AGENT_SERVICE_PUB_KEY` in `.env.e2e`.
1246
1253
 
1247
1254
  ---
1248
1255
 
1249
1256
  ## Troubleshooting
1250
1257
 
1251
- **"Missing CEF_AUTH_TOKEN" on deploy**
1252
- Create a `.env` file next to your `cef.config.yaml` with auth tokens. See [Environment Variables](#environment-variables).
1258
+ **"Not authenticated" on deploy**
1259
+ Run `cef login` to authenticate via browser. See [Authentication](#authentication).
1253
1260
 
1254
1261
  **Stale IDs after copying config between services**
1255
1262
  If you copied a `cef.config.yaml` from another service, the embedded IDs point to the wrong service. Run `cef deploy` and accept the prompt to clear all IDs, or manually remove all `*Id` fields from the config.
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { Command } from 'commander';
6
+ import { registerAuthCommands } from './commands/auth/index.js';
6
7
  import { registerDeployCommands } from './commands/deploy/index.js';
7
8
  import { registerServiceCommands } from './commands/service/index.js';
8
9
  import { registerPlaygroundCommands } from './commands/playground/index.js';
@@ -24,6 +25,7 @@ function showWelcome(version) {
24
25
 
25
26
  \x1b[36mQuick start:\x1b[0m
26
27
 
28
+ \x1b[33m$\x1b[0m cef login Authenticate with CEF platform
27
29
  \x1b[33m$\x1b[0m cef dev Start local dev server with UI
28
30
  \x1b[33m$\x1b[0m cef deploy Deploy topology to orchestrator
29
31
  \x1b[33m$\x1b[0m cef status Compare local vs deployed state
@@ -45,6 +47,7 @@ export function createProgram() {
45
47
  .name('cef')
46
48
  .description('CEF AI command-line interface')
47
49
  .version(getVersion(), '-V, --version');
50
+ registerAuthCommands(program);
48
51
  registerDeployCommands(program);
49
52
  registerServiceCommands(program);
50
53
  registerPlaygroundCommands(program);
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerAuthCommands(program: Command): void;
@@ -0,0 +1,162 @@
1
+ import http from 'http';
2
+ import { exec } from 'child_process';
3
+ import { saveCredentials, loadCredentials, clearCredentials, parseToken, getCredentialsPath, } from '../../lib/credentials.js';
4
+ const DEFAULT_ROB_URL = 'https://rob.compute.test.ddcdragon.com';
5
+ const LOGIN_TIMEOUT_MS = 300_000;
6
+ function openBrowser(url) {
7
+ const cmd = process.platform === 'darwin'
8
+ ? `open "${url}"`
9
+ : process.platform === 'win32'
10
+ ? `start "" "${url}"`
11
+ : `xdg-open "${url}"`;
12
+ exec(cmd, (err) => {
13
+ if (err) {
14
+ console.log(` Could not open browser automatically.`);
15
+ console.log(` Open this URL manually:\n ${url}\n`);
16
+ }
17
+ });
18
+ }
19
+ function saveAndPrint(token) {
20
+ const parsed = parseToken(token);
21
+ if (!parsed) {
22
+ console.error(' Error: Invalid token format. Expected: base64.base64.signature');
23
+ process.exit(1);
24
+ }
25
+ const expiresAt = new Date(parsed.expiresAt).getTime();
26
+ if (expiresAt <= Date.now()) {
27
+ console.error(' Error: Token is already expired.');
28
+ process.exit(1);
29
+ }
30
+ saveCredentials({
31
+ email: parsed.email,
32
+ publicKey: parsed.publicKey,
33
+ token,
34
+ expiresAt: parsed.expiresAt,
35
+ });
36
+ const expiresIn = Math.round((expiresAt - Date.now()) / 3600000);
37
+ console.log(`\n Logged in as ${parsed.email}`);
38
+ console.log(` Token expires in ~${expiresIn}h`);
39
+ console.log(` Credentials saved to ${getCredentialsPath()}\n`);
40
+ }
41
+ function loginWithCallback(robUrl) {
42
+ return new Promise((resolve, reject) => {
43
+ let port = 0;
44
+ const server = http.createServer((req, res) => {
45
+ // CORS preflight
46
+ if (req.method === 'OPTIONS') {
47
+ res.writeHead(204, {
48
+ 'Access-Control-Allow-Origin': '*',
49
+ 'Access-Control-Allow-Methods': 'POST',
50
+ 'Access-Control-Allow-Headers': 'Content-Type',
51
+ });
52
+ res.end();
53
+ return;
54
+ }
55
+ if (req.method === 'POST' && req.url === '/callback') {
56
+ let body = '';
57
+ req.on('data', (chunk) => { body += chunk; });
58
+ req.on('end', () => {
59
+ try {
60
+ const { token } = JSON.parse(body);
61
+ if (!token)
62
+ throw new Error('Missing token');
63
+ res.writeHead(200, {
64
+ 'Content-Type': 'application/json',
65
+ 'Access-Control-Allow-Origin': '*',
66
+ });
67
+ res.end(JSON.stringify({ ok: true }));
68
+ saveAndPrint(token);
69
+ server.close();
70
+ resolve();
71
+ }
72
+ catch {
73
+ res.writeHead(400, {
74
+ 'Content-Type': 'application/json',
75
+ 'Access-Control-Allow-Origin': '*',
76
+ });
77
+ res.end(JSON.stringify({ error: 'Invalid request' }));
78
+ }
79
+ });
80
+ return;
81
+ }
82
+ res.writeHead(404);
83
+ res.end();
84
+ });
85
+ server.on('error', (err) => {
86
+ reject(err);
87
+ });
88
+ server.listen(0, '127.0.0.1', () => {
89
+ const addr = server.address();
90
+ port = typeof addr === 'object' ? addr.port : 0;
91
+ const authUrl = `${robUrl}/#/cli-auth?port=${port}`;
92
+ console.log(`\n Opening browser for authentication...`);
93
+ console.log(` If the browser doesn't open, visit:\n ${authUrl}\n`);
94
+ console.log(` Waiting for login...`);
95
+ openBrowser(authUrl);
96
+ });
97
+ const timeout = setTimeout(() => {
98
+ server.close();
99
+ reject(new Error('Login timed out. Try again with: cef login'));
100
+ }, LOGIN_TIMEOUT_MS);
101
+ server.on('close', () => clearTimeout(timeout));
102
+ });
103
+ }
104
+ async function handleLogin(opts) {
105
+ if (opts.token) {
106
+ saveAndPrint(opts.token);
107
+ return;
108
+ }
109
+ await loginWithCallback(DEFAULT_ROB_URL);
110
+ }
111
+ function handleLogout() {
112
+ const cleared = clearCredentials();
113
+ if (cleared) {
114
+ console.log('\n Logged out. Credentials cleared.\n');
115
+ }
116
+ else {
117
+ console.log('\n Not logged in (no credentials found).\n');
118
+ }
119
+ }
120
+ function handleWhoami() {
121
+ const creds = loadCredentials();
122
+ if (!creds) {
123
+ console.log('\n Not logged in. Run `cef login` to authenticate.\n');
124
+ return;
125
+ }
126
+ const expiresAt = new Date(creds.expiresAt);
127
+ const msLeft = expiresAt.getTime() - Date.now();
128
+ const hoursLeft = Math.max(0, Math.round(msLeft / 3600000));
129
+ const minutesLeft = Math.max(0, Math.round(msLeft / 60000));
130
+ console.log(`\n Email: ${creds.email}`);
131
+ console.log(` Public Key: ${creds.publicKey}`);
132
+ if (minutesLeft < 60) {
133
+ console.log(` Expires in: ${minutesLeft} minutes`);
134
+ }
135
+ else {
136
+ console.log(` Expires in: ~${hoursLeft} hours`);
137
+ }
138
+ console.log(` Stored at: ${getCredentialsPath()}\n`);
139
+ }
140
+ export function registerAuthCommands(program) {
141
+ program
142
+ .command('login')
143
+ .description('Authenticate with CEF platform')
144
+ .option('--token <token>', 'Manually provide a bearer token')
145
+ .action(async (opts) => {
146
+ try {
147
+ await handleLogin(opts);
148
+ }
149
+ catch (err) {
150
+ console.error(`\n Login failed: ${err.message}\n`);
151
+ process.exit(1);
152
+ }
153
+ });
154
+ program
155
+ .command('logout')
156
+ .description('Clear stored credentials')
157
+ .action(() => handleLogout());
158
+ program
159
+ .command('whoami')
160
+ .description('Show current authentication status')
161
+ .action(() => handleWhoami());
162
+ }
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { parse } from 'yaml';
4
4
  import dotenv from 'dotenv';
5
5
  import { toCamelCase } from './utils.js';
6
+ import { loadCredentials } from './credentials.js';
6
7
  const VALID_ALIAS_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
7
8
  export function validateConfig(config) {
8
9
  if (!config || typeof config !== 'object') {
@@ -44,16 +45,27 @@ export function validateConfig(config) {
44
45
  }
45
46
  export function loadEnv() {
46
47
  let authToken = (process.env['CEF_AUTH_TOKEN'] ?? '').trim();
47
- if (!authToken)
48
- throw new Error('Missing required env var: CEF_AUTH_TOKEN');
49
48
  if (authToken.toLowerCase().startsWith('bearer ')) {
50
49
  authToken = authToken.slice(7).trim();
51
50
  }
51
+ // Fallback to stored credentials from `cef login`
52
+ if (!authToken) {
53
+ const creds = loadCredentials();
54
+ if (creds) {
55
+ authToken = creds.token;
56
+ const msLeft = new Date(creds.expiresAt).getTime() - Date.now();
57
+ if (msLeft < 3600000) {
58
+ console.warn('Warning: Token expires in less than 1 hour. Run `cef login` to refresh.');
59
+ }
60
+ }
61
+ }
62
+ if (!authToken)
63
+ throw new Error('Not authenticated. Run `cef login` to authenticate.');
52
64
  let orchestratorUrl = (process.env['CEF_ORCHESTRATOR_URL'] ?? '').trim();
53
65
  if (!orchestratorUrl)
54
- throw new Error('Missing required env var: CEF_ORCHESTRATOR_URL');
66
+ orchestratorUrl = 'https://orchestrator.compute.test.ddcdragon.com';
55
67
  orchestratorUrl = orchestratorUrl.replace(/\/orchestrator\/?$/, '');
56
- const robApiUrl = (process.env['CEF_ROB_API_URL'] ?? '').trim();
68
+ const robApiUrl = (process.env['CEF_ROB_API_URL'] ?? '').trim() || 'https://rob-api.compute.test.ddcdragon.com/rms-node-backend';
57
69
  const robOrigin = (process.env['CEF_ROB_ORIGIN'] ?? '').trim() || undefined;
58
70
  return { authToken, orchestratorUrl, robApiUrl, robOrigin };
59
71
  }
@@ -0,0 +1,19 @@
1
+ export interface StoredCredentials {
2
+ email: string;
3
+ publicKey: string;
4
+ token: string;
5
+ expiresAt: string;
6
+ }
7
+ export declare function getCredentialsPath(): string;
8
+ export declare function saveCredentials(creds: StoredCredentials): void;
9
+ export declare function loadCredentials(): StoredCredentials | null;
10
+ export declare function clearCredentials(): boolean;
11
+ /**
12
+ * Parse a wallet JWT token and extract payload fields.
13
+ * Token format: base64(header).base64(payload).signature
14
+ */
15
+ export declare function parseToken(token: string): {
16
+ email: string;
17
+ publicKey: string;
18
+ expiresAt: string;
19
+ } | null;
@@ -0,0 +1,68 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ function getCefDir() {
5
+ return path.join(os.homedir(), '.cef');
6
+ }
7
+ export function getCredentialsPath() {
8
+ return path.join(getCefDir(), 'credentials.json');
9
+ }
10
+ export function saveCredentials(creds) {
11
+ const dir = getCefDir();
12
+ if (!fs.existsSync(dir)) {
13
+ fs.mkdirSync(dir, { recursive: true });
14
+ }
15
+ fs.writeFileSync(path.join(dir, 'credentials.json'), JSON.stringify(creds, null, 2), 'utf8');
16
+ }
17
+ export function loadCredentials() {
18
+ const credFile = getCredentialsPath();
19
+ if (!fs.existsSync(credFile)) {
20
+ return null;
21
+ }
22
+ try {
23
+ const raw = fs.readFileSync(credFile, 'utf8');
24
+ const creds = JSON.parse(raw);
25
+ if (!creds.token || !creds.expiresAt) {
26
+ return null;
27
+ }
28
+ // Check expiry
29
+ const expiresAt = new Date(creds.expiresAt).getTime();
30
+ if (isNaN(expiresAt) || expiresAt <= Date.now()) {
31
+ return null;
32
+ }
33
+ return creds;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export function clearCredentials() {
40
+ const credFile = getCredentialsPath();
41
+ if (fs.existsSync(credFile)) {
42
+ fs.unlinkSync(credFile);
43
+ return true;
44
+ }
45
+ return false;
46
+ }
47
+ /**
48
+ * Parse a wallet JWT token and extract payload fields.
49
+ * Token format: base64(header).base64(payload).signature
50
+ */
51
+ export function parseToken(token) {
52
+ try {
53
+ const parts = token.split('.');
54
+ if (parts.length !== 3 || !parts[1])
55
+ return null;
56
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
57
+ if (!payload.email || !payload.publicKey || !payload.exp)
58
+ return null;
59
+ return {
60
+ email: payload.email,
61
+ publicKey: payload.publicKey,
62
+ expiresAt: new Date(payload.exp).toISOString(),
63
+ };
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cef-ai/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "CEF AI command-line interface",
5
5
  "type": "module",
6
6
  "bin": {