@latentforce/shift 1.0.5 → 1.0.7

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
@@ -1,9 +1,6 @@
1
1
  # @latentforce/shift
2
2
 
3
- A CLI and MCP server for AI-powered code intelligence. It provides:
4
-
5
- 1. **CLI commands** - Index and manage your project
6
- 2. **MCP server** - Expose tools (blast_radius, dependencies, file_summary) to Claude Desktop/Code
3
+ AI-powered code intelligence CLI and MCP server for Claude.
7
4
 
8
5
  ## Installation
9
6
 
@@ -13,191 +10,77 @@ npm install -g @latentforce/shift
13
10
 
14
11
  ## Quick Start
15
12
 
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://dev-shift-lite.latentforce.ai
22
- shift config set orch-url https://agent-orch.latentforce.ai
23
- shift config set ws-url wss://agent-orch.latentforce.ai
24
- ```
25
-
26
- ### Step 2: Start and configure your project
13
+ ### 1. Initialize your project
27
14
 
28
15
  ```bash
29
16
  cd /path/to/your/project
30
- shift start
17
+ shift-cli start # Configure API key and project
18
+ shift-cli init # Index project files
31
19
  ```
32
20
 
33
- This will:
34
- - Prompt for your Shift API key or let you continue as guest
35
- - Let you select or enter your project ID
36
- - Start a background daemon that connects to the backend
37
-
38
- ### Step 3: Index your project
21
+ ### 2. Add to Claude Code
39
22
 
40
23
  ```bash
41
- shift init
24
+ claude mcp add-json shift '{"type":"stdio","command":"shift-cli","args":["mcp"],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID"}}'
42
25
  ```
43
26
 
44
- This scans your project files and sends the structure to the backend for indexing.
45
-
46
- ### Step 4: Configure MCP for Claude
47
-
48
- Now Claude can use the MCP tools. See [Claude Desktop setup](#claude-desktop-setup) or [Claude Code setup](#claude-code-setup) below.
49
-
50
- ## CLI Commands
51
-
52
- | Command | Description |
53
- |---------|-------------|
54
- | `shift start` | Start the daemon and configure the project |
55
- | `shift init` | Scan and index project files to backend |
56
- | `shift stop` | Stop the running daemon |
57
- | `shift status` | Show current status (API key, project, daemon, connection) |
58
- | `shift config` | Show/set configuration (URLs, API key) |
59
- | `shift` or `shift mcp` | Start MCP server on stdio (used by Claude) |
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
-
70
- ## MCP Tools
71
-
72
- The MCP server exposes these tools to Claude:
73
-
74
- | Tool | Description |
75
- |------|-------------|
76
- | `blast_radius` | Analyze what would be affected if a file is modified |
77
- | `dependencies` | Get all dependencies for a file (direct and transitive) |
78
- | `file_summary` | Generate a summary of a file with optional parent context |
79
-
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`:
27
+ Or using npx:
85
28
 
86
29
  ```bash
87
- # Show current configuration
88
- shift config
89
-
90
- # Set backend URLs
91
- shift config set api-url https://dev-shift-lite.latentforce.ai
92
- shift config set orch-url https://agent-orch.latentforce.ai
93
- shift config set ws-url wss://agent-orch.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)
30
+ claude mcp add-json shift '{"type":"stdio","command":"npx","args":["@latentforce/shift","mcp"],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID"}}'
101
31
  ```
102
32
 
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):
33
+ ### 3. Add to Claude Desktop
108
34
 
109
- | Variable | Description | Default |
110
- |----------|-------------|---------|
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://dev-shift-lite.latentforce.ai
119
- export SHIFT_ORCH_URL=https://agent-orch.latentforce.ai
120
- export SHIFT_WS_URL=wss://agent-orch.latentforce.ai
121
-
122
- # Or inline when running commands
123
- SHIFT_API_URL=https://dev-shift-lite.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)
132
-
133
- ## Claude Code Setup
134
-
135
- ### Add MCP Server
136
-
137
- ```bash
138
- claude mcp add-json shift '{"type":"stdio","command":"shift","args":[],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID_HERE","SHIFT_BACKEND_URL":"https://dev-shift-lite.latentforce.ai"}}'
139
- ```
140
-
141
- ### Verify
142
-
143
- ```bash
144
- claude mcp list
145
- ```
146
-
147
- ## Claude Desktop Setup
148
-
149
- ### Config File Location
150
-
151
- | OS | Path |
152
- |----|------|
153
- | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
154
- | Linux | `~/.config/claude-desktop/claude_desktop_config.json` |
155
- | Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
156
-
157
- ### Configuration
158
-
159
- Add to your config file:
35
+ Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Windows, `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
160
36
 
161
37
  ```json
162
38
  {
163
39
  "mcpServers": {
164
40
  "shift": {
165
- "command": "shift",
41
+ "command": "shift-cli",
42
+ "args": ["mcp"],
166
43
  "env": {
167
- "SHIFT_BACKEND_URL": "https://dev-shift-lite.latentforce.ai",
168
- "SHIFT_PROJECT_ID": "YOUR_PROJECT_ID_HERE"
44
+ "SHIFT_PROJECT_ID": "YOUR_PROJECT_ID"
169
45
  }
170
46
  }
171
47
  }
172
48
  }
173
49
  ```
174
50
 
175
- Restart Claude Desktop after saving.
51
+ ## MCP Tools
176
52
 
177
- ## Project ID
53
+ | Tool | Description |
54
+ |------|-------------|
55
+ | `blast_radius` | Analyze what would be affected if a file is modified |
56
+ | `dependencies` | Get all dependencies for a file |
57
+ | `file_summary` | Generate a summary of a file |
178
58
 
179
- You can set the project ID in two ways:
59
+ ## CLI Commands
180
60
 
181
- 1. **Environment variable**: Set `SHIFT_PROJECT_ID` in MCP config
182
- 2. **Per-call override**: Pass `project_id` parameter in each tool call
61
+ | Command | Description |
62
+ |---------|-------------|
63
+ | `shift-cli start` | Start daemon and configure project |
64
+ | `shift-cli init` | Index project files |
65
+ | `shift-cli stop` | Stop the daemon |
66
+ | `shift-cli status` | Show current status |
67
+ | `shift-cli config` | Manage configuration |
183
68
 
184
- This allows one MCP server to work with multiple projects.
69
+ ## Custom Backend URLs
185
70
 
186
- ## Development
71
+ **CLI** (set once globally):
187
72
 
188
73
  ```bash
189
- # Clone and install
190
- npm install
191
-
192
- # Build
193
- npm run build
74
+ shift-cli config set api-url https://your-api.example.com
75
+ shift-cli config set orch-url https://your-orch.example.com
76
+ shift-cli config set ws-url wss://your-orch.example.com
77
+ ```
194
78
 
195
- # Run locally
196
- node build/index.js
79
+ **MCP Server** (add to env in Claude config):
197
80
 
198
- # Test CLI commands
199
- node build/index.js start
200
- node build/index.js init
201
- node build/index.js status
202
- node build/index.js stop
81
+ ```json
82
+ "env": {
83
+ "SHIFT_PROJECT_ID": "YOUR_PROJECT_ID",
84
+ "SHIFT_BACKEND_URL": "https://your-api.example.com"
85
+ }
203
86
  ```
@@ -1,13 +1,29 @@
1
1
  import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
+ import { createInterface } from 'readline';
3
4
  import { getApiKey, setApiKey, setGuestKey, isGuestKey, readProjectConfig, writeProjectConfig } from '../../utils/config.js';
4
5
  import { promptApiKey, promptKeyChoice } from '../../utils/prompts.js';
5
- import { requestGuestKey } from '../../utils/api-client.js';
6
+ import { requestGuestKey, fetchProjectStatus } from '../../utils/api-client.js';
6
7
  import { getDaemonStatus } from '../../daemon/daemon-manager.js';
7
8
  import { getProjectTree, extractAllFilePaths, categorizeFiles } from '../../utils/tree-scanner.js';
8
9
  import { sendInitScan } from '../../utils/api-client.js';
9
10
  const execAsync = promisify(exec);
10
- export async function initCommand() {
11
+ /**
12
+ * Prompt user to confirm re-indexing an already indexed project
13
+ */
14
+ async function promptForReindex() {
15
+ const rl = createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout,
18
+ });
19
+ return new Promise((resolve) => {
20
+ rl.question('Do you want to re-index the project? (y/N): ', (answer) => {
21
+ rl.close();
22
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
23
+ });
24
+ });
25
+ }
26
+ export async function initCommand(options = {}) {
11
27
  const projectRoot = process.cwd();
12
28
  console.log('╔═══════════════════════════════════════════════╗');
13
29
  console.log('║ Initializing Shift Project ║');
@@ -51,6 +67,32 @@ export async function initCommand() {
51
67
  process.exit(1);
52
68
  }
53
69
  console.log(`[Init] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
70
+ // Check if project is already indexed (skip check if --force flag is used)
71
+ if (!options.force) {
72
+ try {
73
+ const projectStatus = await fetchProjectStatus(apiKey, projectConfig.project_id);
74
+ if (projectStatus.indexed) {
75
+ console.log(`[Init] ✓ Project already indexed (${projectStatus.file_count} files)\n`);
76
+ const shouldReindex = await promptForReindex();
77
+ if (!shouldReindex) {
78
+ console.log('\n╔═══════════════════════════════════════════════╗');
79
+ console.log('║ Project Already Initialized ║');
80
+ console.log('╚═══════════════════════════════════════════════╝');
81
+ console.log('\nUse "shift status" to check the current status.');
82
+ console.log('Use "shift init --force" to force re-indexing.\n');
83
+ return;
84
+ }
85
+ console.log('\n[Init] Proceeding with re-indexing...\n');
86
+ }
87
+ }
88
+ catch {
89
+ // If we can't check status, continue with init (server might be unavailable)
90
+ console.log('[Init] ⚠️ Could not check indexing status, proceeding with initialization...\n');
91
+ }
92
+ }
93
+ else {
94
+ console.log('[Init] Force flag detected, skipping indexing status check...\n');
95
+ }
54
96
  // Step 3: Check daemon status (warn if not running)
55
97
  console.log('[Init] Step 3/5: Checking daemon status...');
56
98
  const status = getDaemonStatus(projectRoot);
@@ -1,5 +1,6 @@
1
1
  import { getApiKey, readProjectConfig, isGuestKey } from '../../utils/config.js';
2
2
  import { getDaemonStatus } from '../../daemon/daemon-manager.js';
3
+ import { fetchProjectStatus } from '../../utils/api-client.js';
3
4
  export async function statusCommand() {
4
5
  const projectRoot = process.cwd();
5
6
  console.log('\n╔════════════════════════════════════════════╗');
@@ -34,6 +35,20 @@ export async function statusCommand() {
34
35
  console.log(` - ${agent.agent_name} (${agent.agent_type})`);
35
36
  });
36
37
  }
38
+ // Check project indexing status (work done)
39
+ try {
40
+ const projectStatus = await fetchProjectStatus(apiKey, projectConfig.project_id);
41
+ if (projectStatus.indexed) {
42
+ console.log(`Indexed: ✓ Complete (${projectStatus.file_count} files)`);
43
+ }
44
+ else {
45
+ console.log('Indexed: ❌ Not indexed');
46
+ console.log(' Run "shift-cli init" to scan the project.');
47
+ }
48
+ }
49
+ catch {
50
+ console.log('Indexed: ⚠️ Unable to check (server unavailable)');
51
+ }
37
52
  // Check daemon status
38
53
  const status = getDaemonStatus(projectRoot);
39
54
  if (!status.running) {
package/build/index.js CHANGED
@@ -2,14 +2,21 @@
2
2
  import { Command } from 'commander';
3
3
  const program = new Command();
4
4
  program
5
- .name('shift')
5
+ .name('shift-cli')
6
6
  .description('Shift CLI - AI-powered code intelligence')
7
- .version('1.0.2');
7
+ .version('1.0.6');
8
8
  // MCP server mode (default when run via MCP host)
9
9
  program
10
10
  .command('mcp', { isDefault: true, hidden: true })
11
11
  .description('Start MCP server on stdio')
12
12
  .action(async () => {
13
+ // Check if 'mcp' was explicitly passed as argument
14
+ const mcpExplicitlyRequested = process.argv.includes('mcp');
15
+ // If running interactively (TTY) and mcp wasn't explicitly requested, show help
16
+ if (process.stdin.isTTY && !mcpExplicitlyRequested) {
17
+ program.outputHelp();
18
+ return;
19
+ }
13
20
  const { startMcpServer } = await import('./mcp-server.js');
14
21
  await startMcpServer();
15
22
  });
@@ -24,9 +31,10 @@ program
24
31
  program
25
32
  .command('init')
26
33
  .description('Initialize and scan the project for file indexing')
27
- .action(async () => {
34
+ .option('-f, --force', 'Force re-indexing even if project is already indexed')
35
+ .action(async (options) => {
28
36
  const { initCommand } = await import('./cli/commands/init.js');
29
- await initCommand();
37
+ await initCommand({ force: options.force ?? false });
30
38
  });
31
39
  program
32
40
  .command('stop')
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { z } from 'zod';
4
- const BASE_URL = process.env.SHIFT_BACKEND_URL || "http://127.0.0.1:9000";
4
+ const BASE_URL = process.env.SHIFT_BACKEND_URL || "https://dev-shift-lite.latentforce.ai";
5
5
  function getProjectIdFromEnv() {
6
6
  const projectId = process.env.SHIFT_PROJECT_ID;
7
7
  if (!projectId || projectId.trim() === "") {
@@ -1,13 +1,23 @@
1
1
  import { API_BASE_URL, API_BASE_URL_ORCH } from './config.js';
2
+ import { getMachineFingerprint } from './machine-id.js';
2
3
  /**
3
- * Request a guest API key
4
+ * Request a guest API key bound to this machine
5
+ * The backend will:
6
+ * - Create a new guest key if this machine_id hasn't been seen before
7
+ * - Return the existing guest key if this machine_id already has one
8
+ * - Optionally enforce usage limits per machine
4
9
  */
5
10
  export async function requestGuestKey() {
11
+ const fingerprint = getMachineFingerprint();
6
12
  const response = await fetch(`${API_BASE_URL}/api/guest-key`, {
7
13
  method: 'POST',
8
14
  headers: {
9
15
  'Content-Type': 'application/json',
10
16
  },
17
+ body: JSON.stringify({
18
+ machine_id: fingerprint.machine_id,
19
+ platform: fingerprint.platform,
20
+ }),
11
21
  });
12
22
  if (!response.ok) {
13
23
  const text = await response.text();
@@ -64,3 +74,30 @@ export async function sendInitScan(apiKey, projectId, payload) {
64
74
  throw error;
65
75
  }
66
76
  }
77
+ /**
78
+ * Fetch project indexing/work status
79
+ * Returns whether the knowledge graph has been built for this project
80
+ */
81
+ export async function fetchProjectStatus(apiKey, projectId) {
82
+ const controller = new AbortController();
83
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
84
+ try {
85
+ const response = await fetch(`${API_BASE_URL}/api/projects/${projectId}/status`, {
86
+ method: 'GET',
87
+ headers: {
88
+ 'Authorization': `Bearer ${apiKey}`,
89
+ },
90
+ signal: controller.signal,
91
+ });
92
+ clearTimeout(timeoutId);
93
+ if (!response.ok) {
94
+ const text = await response.text();
95
+ throw new Error(text || `HTTP ${response.status}`);
96
+ }
97
+ return await response.json();
98
+ }
99
+ catch (error) {
100
+ clearTimeout(timeoutId);
101
+ throw error;
102
+ }
103
+ }
@@ -0,0 +1,46 @@
1
+ import os from 'os';
2
+ import crypto from 'crypto';
3
+ /**
4
+ * Get the primary MAC address (first non-internal interface)
5
+ */
6
+ function getPrimaryMacAddress() {
7
+ const interfaces = os.networkInterfaces();
8
+ for (const [name, addrs] of Object.entries(interfaces)) {
9
+ if (!addrs)
10
+ continue;
11
+ for (const addr of addrs) {
12
+ // Skip internal/loopback and addresses without MAC
13
+ if (addr.internal || !addr.mac || addr.mac === '00:00:00:00:00:00') {
14
+ continue;
15
+ }
16
+ return addr.mac;
17
+ }
18
+ }
19
+ return 'unknown';
20
+ }
21
+ /**
22
+ * Generate a unique, deterministic machine fingerprint
23
+ * Uses MAC address + platform for stability
24
+ * - MAC is globally unique per network interface
25
+ * - Platform helps distinguish dual-boot scenarios
26
+ */
27
+ export function generateMachineId() {
28
+ const mac = getPrimaryMacAddress();
29
+ const platform = os.platform();
30
+ // Combine MAC + platform
31
+ const fingerprintData = `${mac}|${platform}`;
32
+ // Hash for privacy and fixed length
33
+ return crypto.createHash('sha256').update(fingerprintData).digest('hex');
34
+ }
35
+ /**
36
+ * Generate a shorter machine ID (first 16 chars of full hash)
37
+ */
38
+ export function generateShortMachineId() {
39
+ return generateMachineId().substring(0, 16);
40
+ }
41
+ export function getMachineFingerprint() {
42
+ return {
43
+ machine_id: generateMachineId(),
44
+ platform: os.platform(),
45
+ };
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latentforce/shift",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Shift CLI - AI-powered code intelligence with MCP support",
5
5
  "type": "module",
6
6
  "main": "./build/index.js",
@@ -8,7 +8,7 @@
8
8
  ".": "./build/index.js"
9
9
  },
10
10
  "bin": {
11
- "shift": "./build/index.js"
11
+ "shift-cli": "./build/index.js"
12
12
  },
13
13
  "files": [
14
14
  "build"