@latentforce/shift 1.0.2 → 1.0.3

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
@@ -13,7 +13,17 @@ npm install -g @latentforce/shift
13
13
 
14
14
  ## Quick Start
15
15
 
16
- ### Step 1: Start and configure your project
16
+ ### Step 1: Configure backend URLs (if using hosted service)
17
+
18
+ If you're connecting to a hosted Shift backend (not localhost), configure the URLs first:
19
+
20
+ ```bash
21
+ shift config set api-url https://api.latentforce.ai
22
+ shift config set orch-url https://orch.latentforce.ai
23
+ shift config set ws-url wss://ws.latentforce.ai
24
+ ```
25
+
26
+ ### Step 2: Start and configure your project
17
27
 
18
28
  ```bash
19
29
  cd /path/to/your/project
@@ -21,11 +31,11 @@ shift start
21
31
  ```
22
32
 
23
33
  This will:
24
- - Prompt for your Shift API key (saved to `~/.shift/config.json`)
34
+ - Prompt for your Shift API key or let you continue as guest
25
35
  - Let you select or enter your project ID
26
36
  - Start a background daemon that connects to the backend
27
37
 
28
- ### Step 2: Index your project
38
+ ### Step 3: Index your project
29
39
 
30
40
  ```bash
31
41
  shift init
@@ -33,7 +43,7 @@ shift init
33
43
 
34
44
  This scans your project files and sends the structure to the backend for indexing.
35
45
 
36
- ### Step 3: Configure MCP for Claude
46
+ ### Step 4: Configure MCP for Claude
37
47
 
38
48
  Now Claude can use the MCP tools. See [Claude Desktop setup](#claude-desktop-setup) or [Claude Code setup](#claude-code-setup) below.
39
49
 
@@ -45,8 +55,18 @@ Now Claude can use the MCP tools. See [Claude Desktop setup](#claude-desktop-set
45
55
  | `shift init` | Scan and index project files to backend |
46
56
  | `shift stop` | Stop the running daemon |
47
57
  | `shift status` | Show current status (API key, project, daemon, connection) |
58
+ | `shift config` | Show/set configuration (URLs, API key) |
48
59
  | `shift` or `shift mcp` | Start MCP server on stdio (used by Claude) |
49
60
 
61
+ ## Authentication
62
+
63
+ When running `shift start` or `shift init`, you'll be prompted to choose:
64
+
65
+ 1. **API Key** - Enter your Shift API key for full access
66
+ 2. **Guest Mode** - Continue without an API key (limited features)
67
+
68
+ Both options work with all CLI commands. Your choice is saved to `~/.shift/config.json`.
69
+
50
70
  ## MCP Tools
51
71
 
52
72
  The MCP server exposes these tools to Claude:
@@ -57,15 +77,58 @@ The MCP server exposes these tools to Claude:
57
77
  | `dependencies` | Get all dependencies for a file (direct and transitive) |
58
78
  | `file_summary` | Generate a summary of a file with optional parent context |
59
79
 
60
- ## Environment Variables
80
+ ## Configuration
81
+
82
+ ### Using the Config Command (Recommended for Global Install)
83
+
84
+ The easiest way to configure Shift after installing globally with `npm i -g @latentforce/shift`:
85
+
86
+ ```bash
87
+ # Show current configuration
88
+ shift config
89
+
90
+ # Set backend URLs
91
+ shift config set api-url https://api.latentforce.ai
92
+ shift config set orch-url https://orch.latentforce.ai
93
+ shift config set ws-url wss://ws.latentforce.ai
94
+
95
+ # Set API key directly (alternative to interactive prompt)
96
+ shift config set api-key your-api-key-here
97
+
98
+ # Clear configuration
99
+ shift config clear urls # Reset URLs to defaults
100
+ shift config clear # Clear all config (prompts for confirmation)
101
+ ```
102
+
103
+ Configuration is saved to `~/.shift/config.json`.
104
+
105
+ ### Using Environment Variables
106
+
107
+ You can also set environment variables (they take priority over config file):
61
108
 
62
109
  | Variable | Description | Default |
63
110
  |----------|-------------|---------|
64
- | `SHIFT_PROJECT_ID` | Default project UUID (can be overridden per tool call) | - |
65
- | `SHIFT_BACKEND_URL` | Backend API URL | `http://127.0.0.1:9000` |
66
- | `SHIFT_API_URL` | API URL for CLI | `http://localhost:9000` |
67
- | `SHIFT_ORCH_URL` | Orchestrator URL | `http://localhost:9999` |
68
- | `SHIFT_WS_URL` | WebSocket URL | `ws://localhost:9999` |
111
+ | `SHIFT_PROJECT_ID` | Default project UUID (MCP only) | - |
112
+ | `SHIFT_BACKEND_URL` | Backend API URL (MCP only) | `http://127.0.0.1:9000` |
113
+
114
+ Example using environment variables:
115
+
116
+ ```bash
117
+ # Set for current session
118
+ export SHIFT_API_URL=https://api.latentforce.ai
119
+ export SHIFT_ORCH_URL=https://orch.latentforce.ai
120
+ export SHIFT_WS_URL=wss://ws.latentforce.ai
121
+
122
+ # Or inline when running commands
123
+ SHIFT_API_URL=https://api.latentforce.ai shift start
124
+ ```
125
+
126
+ ### Priority Order
127
+
128
+ URLs are resolved in this order:
129
+ 1. Environment variables (highest priority)
130
+ 2. Config file (`~/.shift/config.json`)
131
+ 3. Defaults (localhost)
69
132
 
70
133
  ## Claude Code Setup
71
134
 
@@ -0,0 +1,136 @@
1
+ import { readGlobalConfig, setApiKey, setApiUrl, setOrchUrl, setWsUrl, clearUrls, clearApiKey, getConfiguredUrls, isGuestKey, } from '../../utils/config.js';
2
+ import { prompt } from '../../utils/prompts.js';
3
+ export async function configCommand(action, key, value) {
4
+ const parsedAction = (action || 'show');
5
+ switch (parsedAction) {
6
+ case 'show':
7
+ showConfig();
8
+ break;
9
+ case 'set':
10
+ await setConfigValue(key, value);
11
+ break;
12
+ case 'clear':
13
+ await clearConfig(key);
14
+ break;
15
+ default:
16
+ console.log('Usage: shift config [show|set|clear] [key] [value]');
17
+ console.log('');
18
+ console.log('Commands:');
19
+ console.log(' show Show current configuration');
20
+ console.log(' set <key> <value> Set a configuration value');
21
+ console.log(' clear [key] Clear configuration (all or specific key)');
22
+ console.log('');
23
+ console.log('Keys:');
24
+ console.log(' api-key Your Shift API key');
25
+ console.log(' api-url SHIFT_API_URL (default: http://localhost:9000)');
26
+ console.log(' orch-url SHIFT_ORCH_URL (default: http://localhost:9999)');
27
+ console.log(' ws-url SHIFT_WS_URL (default: ws://localhost:9999)');
28
+ console.log('');
29
+ console.log('Examples:');
30
+ console.log(' shift config');
31
+ console.log(' shift config set api-url https://api.latentforce.ai');
32
+ console.log(' shift config set orch-url https://orch.latentforce.ai');
33
+ console.log(' shift config set ws-url wss://ws.latentforce.ai');
34
+ console.log(' shift config clear urls');
35
+ break;
36
+ }
37
+ }
38
+ function showConfig() {
39
+ console.log('╔════════════════════════════════════════════╗');
40
+ console.log('║ Shift Configuration ║');
41
+ console.log('╚════════════════════════════════════════════╝\n');
42
+ const globalConfig = readGlobalConfig();
43
+ const urls = getConfiguredUrls();
44
+ // API Key
45
+ console.log('API Key:');
46
+ if (globalConfig?.api_key) {
47
+ const keyType = isGuestKey() ? '(guest)' : '(paid)';
48
+ const maskedKey = globalConfig.api_key.substring(0, 8) + '...' + globalConfig.api_key.slice(-4);
49
+ console.log(` ${maskedKey} ${keyType}`);
50
+ }
51
+ else {
52
+ console.log(' Not configured');
53
+ }
54
+ console.log('');
55
+ // URLs
56
+ console.log('URLs:');
57
+ console.log(` API URL: ${urls.api_url}`);
58
+ console.log(` Orch URL: ${urls.orch_url}`);
59
+ console.log(` WS URL: ${urls.ws_url}`);
60
+ console.log('');
61
+ // Source info
62
+ console.log('Config file: ~/.shift/config.json');
63
+ console.log('');
64
+ console.log('Tip: URLs can also be set via environment variables:');
65
+ console.log(' SHIFT_API_URL, SHIFT_ORCH_URL, SHIFT_WS_URL');
66
+ }
67
+ async function setConfigValue(key, value) {
68
+ if (!key) {
69
+ console.error('Error: Key is required. Run "shift config" to see available keys.');
70
+ process.exit(1);
71
+ }
72
+ // If no value provided, prompt for it
73
+ if (!value) {
74
+ value = await prompt(`Enter value for ${key}: `);
75
+ if (!value) {
76
+ console.error('Error: Value is required.');
77
+ process.exit(1);
78
+ }
79
+ }
80
+ switch (key) {
81
+ case 'api-key':
82
+ setApiKey(value);
83
+ console.log('✓ API key saved');
84
+ break;
85
+ case 'api-url':
86
+ setApiUrl(value);
87
+ console.log(`✓ API URL set to: ${value}`);
88
+ break;
89
+ case 'orch-url':
90
+ setOrchUrl(value);
91
+ console.log(`✓ Orch URL set to: ${value}`);
92
+ break;
93
+ case 'ws-url':
94
+ setWsUrl(value);
95
+ console.log(`✓ WS URL set to: ${value}`);
96
+ break;
97
+ default:
98
+ console.error(`Unknown key: ${key}`);
99
+ console.log('Available keys: api-key, api-url, orch-url, ws-url');
100
+ process.exit(1);
101
+ }
102
+ console.log('\nNote: Restart any running daemon for URL changes to take effect.');
103
+ }
104
+ async function clearConfig(key) {
105
+ if (!key) {
106
+ // Clear everything
107
+ const answer = await prompt('Clear all configuration? (y/N): ');
108
+ if (answer.toLowerCase() !== 'y') {
109
+ console.log('Cancelled.');
110
+ return;
111
+ }
112
+ clearApiKey();
113
+ clearUrls();
114
+ console.log('✓ All configuration cleared');
115
+ return;
116
+ }
117
+ switch (key) {
118
+ case 'api-key':
119
+ clearApiKey();
120
+ console.log('✓ API key cleared');
121
+ break;
122
+ case 'urls':
123
+ clearUrls();
124
+ console.log('✓ URLs cleared (will use defaults)');
125
+ break;
126
+ case 'api-url':
127
+ case 'orch-url':
128
+ case 'ws-url':
129
+ console.log('Use "shift config clear urls" to clear all URLs');
130
+ break;
131
+ default:
132
+ console.error(`Unknown key: ${key}`);
133
+ console.log('Available keys: api-key, urls');
134
+ process.exit(1);
135
+ }
136
+ }
@@ -1,7 +1,8 @@
1
1
  import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
- import { getApiKey, setApiKey, readProjectConfig, writeProjectConfig } from '../../utils/config.js';
4
- import { promptApiKey } from '../../utils/prompts.js';
3
+ import { getApiKey, setApiKey, setGuestKey, isGuestKey, readProjectConfig, writeProjectConfig } from '../../utils/config.js';
4
+ import { promptApiKey, promptKeyChoice } from '../../utils/prompts.js';
5
+ import { requestGuestKey } from '../../utils/api-client.js';
5
6
  import { getDaemonStatus } from '../../daemon/daemon-manager.js';
6
7
  import { getProjectTree, extractAllFilePaths, categorizeFiles } from '../../utils/tree-scanner.js';
7
8
  import { sendInitScan } from '../../utils/api-client.js';
@@ -11,13 +12,32 @@ export async function initCommand() {
11
12
  console.log('╔═══════════════════════════════════════════════╗');
12
13
  console.log('║ Initializing Shift Project ║');
13
14
  console.log('╚═══════════════════════════════════════════════╝\n');
14
- // Step 1: Check API key
15
+ // Step 1: Check API key (with guest flow support)
15
16
  console.log('[Init] Step 1/5: Checking API key...');
16
17
  let apiKey = getApiKey();
17
18
  if (!apiKey) {
18
- apiKey = await promptApiKey();
19
- setApiKey(apiKey);
20
- console.log('[Init] API key saved\n');
19
+ const choice = await promptKeyChoice();
20
+ if (choice === 'paid') {
21
+ apiKey = await promptApiKey();
22
+ setApiKey(apiKey);
23
+ console.log('[Init] ✓ API key saved to ~/.shift/config.json\n');
24
+ }
25
+ else {
26
+ console.log('\n[Init] Requesting guest session...');
27
+ try {
28
+ const guestResponse = await requestGuestKey();
29
+ setGuestKey(guestResponse.api_key);
30
+ apiKey = guestResponse.api_key;
31
+ console.log('[Init] ✓ Guest session started\n');
32
+ }
33
+ catch (error) {
34
+ console.error(`\n❌ Failed to get guest key: ${error.message}`);
35
+ process.exit(1);
36
+ }
37
+ }
38
+ }
39
+ else if (isGuestKey()) {
40
+ console.log('[Init] ✓ Guest key found\n');
21
41
  }
22
42
  else {
23
43
  console.log('[Init] ✓ API key found\n');
@@ -1,7 +1,7 @@
1
- import { getApiKey, setApiKey, setProject, readProjectConfig } from '../../utils/config.js';
2
- import { promptApiKey, promptProjectId, promptSelectProject } from '../../utils/prompts.js';
1
+ import { getApiKey, setApiKey, setGuestKey, isGuestKey, setProject, readProjectConfig } from '../../utils/config.js';
2
+ import { promptApiKey, promptProjectId, promptSelectProject, promptKeyChoice } from '../../utils/prompts.js';
3
3
  import { startDaemon, getDaemonStatus } from '../../daemon/daemon-manager.js';
4
- import { fetchProjects } from '../../utils/api-client.js';
4
+ import { fetchProjects, requestGuestKey } from '../../utils/api-client.js';
5
5
  export async function startCommand() {
6
6
  const projectRoot = process.cwd();
7
7
  console.log('╔════════════════════════════════════════════╗');
@@ -11,9 +11,28 @@ export async function startCommand() {
11
11
  console.log('[Start] Step 1/4: Checking API key...');
12
12
  let apiKey = getApiKey();
13
13
  if (!apiKey) {
14
- apiKey = await promptApiKey();
15
- setApiKey(apiKey);
16
- console.log('[Start] API key saved to ~/.shift/config.json\n');
14
+ const choice = await promptKeyChoice();
15
+ if (choice === 'paid') {
16
+ apiKey = await promptApiKey();
17
+ setApiKey(apiKey);
18
+ console.log('[Start] ✓ API key saved to ~/.shift/config.json\n');
19
+ }
20
+ else {
21
+ console.log('\n[Start] Requesting guest session...');
22
+ try {
23
+ const guestResponse = await requestGuestKey();
24
+ setGuestKey(guestResponse.api_key);
25
+ apiKey = guestResponse.api_key;
26
+ console.log('[Start] ✓ Guest session started\n');
27
+ }
28
+ catch (error) {
29
+ console.error(`\n❌ Failed to get guest key: ${error.message}`);
30
+ process.exit(1);
31
+ }
32
+ }
33
+ }
34
+ else if (isGuestKey()) {
35
+ console.log('[Start] ✓ Guest key found\n');
17
36
  }
18
37
  else {
19
38
  console.log('[Start] ✓ API key found\n');
@@ -1,4 +1,4 @@
1
- import { getApiKey, readProjectConfig } from '../../utils/config.js';
1
+ import { getApiKey, readProjectConfig, isGuestKey } from '../../utils/config.js';
2
2
  import { getDaemonStatus } from '../../daemon/daemon-manager.js';
3
3
  export async function statusCommand() {
4
4
  const projectRoot = process.cwd();
@@ -12,7 +12,12 @@ export async function statusCommand() {
12
12
  console.log('\nRun "shift start" to configure your API key.\n');
13
13
  return;
14
14
  }
15
- console.log('API Key: ✓ Configured');
15
+ if (isGuestKey()) {
16
+ console.log('API Key: ✓ Guest key');
17
+ }
18
+ else {
19
+ console.log('API Key: ✓ Configured');
20
+ }
16
21
  // Check project config
17
22
  const projectConfig = readProjectConfig(projectRoot);
18
23
  if (!projectConfig) {
package/build/index.js CHANGED
@@ -42,4 +42,11 @@ program
42
42
  const { statusCommand } = await import('./cli/commands/status.js');
43
43
  await statusCommand();
44
44
  });
45
+ program
46
+ .command('config [action] [key] [value]')
47
+ .description('Manage Shift configuration (URLs, API key)')
48
+ .action(async (action, key, value) => {
49
+ const { configCommand } = await import('./cli/commands/config.js');
50
+ await configCommand(action, key, value);
51
+ });
45
52
  program.parse();
@@ -1,4 +1,20 @@
1
1
  import { API_BASE_URL, API_BASE_URL_ORCH } from './config.js';
2
+ /**
3
+ * Request a guest API key
4
+ */
5
+ export async function requestGuestKey() {
6
+ const response = await fetch(`${API_BASE_URL}/api/guest-key`, {
7
+ method: 'POST',
8
+ headers: {
9
+ 'Content-Type': 'application/json',
10
+ },
11
+ });
12
+ if (!response.ok) {
13
+ const text = await response.text();
14
+ throw new Error(text || `HTTP ${response.status}`);
15
+ }
16
+ return await response.json();
17
+ }
2
18
  /**
3
19
  * Fetch available projects for the user
4
20
  * Matching extension's fetchProjects function in api-client.js
@@ -9,10 +9,39 @@ const LOCAL_SHIFT_DIR = '.shift';
9
9
  const LOCAL_CONFIG_FILE = 'config.json'; // Changed from project.json to match extension
10
10
  const DAEMON_PID_FILE = 'daemon.pid';
11
11
  const DAEMON_STATUS_FILE = 'daemon.status.json';
12
- // API URLs - matching extension's config.js
13
- export const API_BASE_URL = process.env.SHIFT_API_URL || 'http://localhost:9000';
14
- export const API_BASE_URL_ORCH = process.env.SHIFT_ORCH_URL || 'http://localhost:9999';
15
- export const WS_URL = process.env.SHIFT_WS_URL || 'ws://localhost:9999';
12
+ // Default URLs
13
+ const DEFAULT_API_URL = 'http://localhost:9000';
14
+ const DEFAULT_ORCH_URL = 'http://localhost:9999';
15
+ const DEFAULT_WS_URL = 'ws://localhost:9999';
16
+ // Helper to get URL from: 1) env var, 2) global config, 3) default
17
+ function getConfiguredUrl(envVar, configKey, defaultUrl) {
18
+ // Priority: Environment variable > Global config > Default
19
+ if (process.env[envVar]) {
20
+ return process.env[envVar];
21
+ }
22
+ const config = readGlobalConfigInternal();
23
+ if (config?.[configKey]) {
24
+ return config[configKey];
25
+ }
26
+ return defaultUrl;
27
+ }
28
+ // Internal read to avoid circular dependency
29
+ function readGlobalConfigInternal() {
30
+ try {
31
+ if (fs.existsSync(GLOBAL_CONFIG_FILE)) {
32
+ const content = fs.readFileSync(GLOBAL_CONFIG_FILE, 'utf-8');
33
+ return JSON.parse(content);
34
+ }
35
+ }
36
+ catch {
37
+ // Invalid config, treat as not existing
38
+ }
39
+ return null;
40
+ }
41
+ // API URLs - Priority: env var > global config > default
42
+ export const API_BASE_URL = getConfiguredUrl('SHIFT_API_URL', 'api_url', DEFAULT_API_URL);
43
+ export const API_BASE_URL_ORCH = getConfiguredUrl('SHIFT_ORCH_URL', 'orch_url', DEFAULT_ORCH_URL);
44
+ export const WS_URL = getConfiguredUrl('SHIFT_WS_URL', 'ws_url', DEFAULT_WS_URL);
16
45
  // --- Global Config Functions ---
17
46
  export function ensureGlobalShiftDir() {
18
47
  if (!fs.existsSync(GLOBAL_SHIFT_DIR)) {
@@ -42,8 +71,56 @@ export function getApiKey() {
42
71
  export function setApiKey(apiKey) {
43
72
  const config = readGlobalConfig() || { api_key: '' };
44
73
  config.api_key = apiKey;
74
+ delete config.key_type;
45
75
  writeGlobalConfig(config);
46
76
  }
77
+ export function setGuestKey(apiKey) {
78
+ const config = {
79
+ api_key: apiKey,
80
+ key_type: 'guest',
81
+ };
82
+ writeGlobalConfig(config);
83
+ }
84
+ export function isGuestKey() {
85
+ const config = readGlobalConfig();
86
+ return config?.key_type === 'guest';
87
+ }
88
+ export function clearApiKey() {
89
+ const config = readGlobalConfig() || { api_key: '' };
90
+ config.api_key = '';
91
+ delete config.key_type;
92
+ writeGlobalConfig(config);
93
+ }
94
+ // --- URL Configuration Functions ---
95
+ export function setApiUrl(url) {
96
+ const config = readGlobalConfig() || { api_key: '' };
97
+ config.api_url = url;
98
+ writeGlobalConfig(config);
99
+ }
100
+ export function setOrchUrl(url) {
101
+ const config = readGlobalConfig() || { api_key: '' };
102
+ config.orch_url = url;
103
+ writeGlobalConfig(config);
104
+ }
105
+ export function setWsUrl(url) {
106
+ const config = readGlobalConfig() || { api_key: '' };
107
+ config.ws_url = url;
108
+ writeGlobalConfig(config);
109
+ }
110
+ export function clearUrls() {
111
+ const config = readGlobalConfig() || { api_key: '' };
112
+ delete config.api_url;
113
+ delete config.orch_url;
114
+ delete config.ws_url;
115
+ writeGlobalConfig(config);
116
+ }
117
+ export function getConfiguredUrls() {
118
+ return {
119
+ api_url: API_BASE_URL,
120
+ orch_url: API_BASE_URL_ORCH,
121
+ ws_url: WS_URL,
122
+ };
123
+ }
47
124
  // --- Local Config Functions ---
48
125
  export function getLocalShiftDir(projectRoot) {
49
126
  const root = projectRoot || process.cwd();
@@ -48,3 +48,22 @@ export async function promptSelectProject(projects) {
48
48
  }
49
49
  return projects[selection - 1];
50
50
  }
51
+ export async function promptKeyChoice() {
52
+ console.log('\nNo Shift API key found.');
53
+ console.log('');
54
+ console.log(' 1. I have an API key');
55
+ console.log(' 2. Continue as guest');
56
+ console.log('');
57
+ const answer = await prompt('Select an option (1 or 2): ');
58
+ const selection = parseInt(answer, 10);
59
+ if (selection === 1) {
60
+ return 'paid';
61
+ }
62
+ else if (selection === 2) {
63
+ return 'guest';
64
+ }
65
+ else {
66
+ console.error('Invalid selection.');
67
+ process.exit(1);
68
+ }
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latentforce/shift",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Shift CLI - AI-powered code intelligence with MCP support",
5
5
  "type": "module",
6
6
  "main": "./build/index.js",