@latentforce/shift 1.0.10 → 1.0.11

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
@@ -40,19 +40,25 @@ shift-cli init # Index project files
40
40
 
41
41
  ## MCP Integration
42
42
 
43
- ### Add to Claude Code
43
+ After initializing your project, use `shift-cli add` to configure Shift's MCP server in your AI coding tool:
44
44
 
45
45
  ```bash
46
- claude mcp add-json shift '{"type":"stdio","command":"shift-cli","args":["mcp"],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID"}}'
46
+ shift-cli add <tool>
47
47
  ```
48
48
 
49
- Or using npx (no global install needed):
49
+ ### Supported tools
50
50
 
51
- ```bash
52
- claude mcp add-json shift '{"type":"stdio","command":"npx","args":["@latentforce/shift","mcp"],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID"}}'
53
- ```
51
+ | Tool | Command | Config method |
52
+ |------|---------|---------------|
53
+ | Claude Code | `shift-cli add claude-code` | Runs `claude mcp add-json` |
54
+ | Opencode | `shift-cli add opencode` | Writes `opencode.json` |
55
+ | Codex | `shift-cli add codex` | Runs `codex mcp add` |
56
+ | GitHub Copilot | `shift-cli add copilot` | Writes `.vscode/mcp.json` |
57
+ | Factory Droid | `shift-cli add droid` | Runs `droid mcp add` |
54
58
 
55
- ### Add to Claude Desktop
59
+ For CLI-based tools, if the CLI is not installed, the command will print manual setup instructions.
60
+
61
+ ### Claude Desktop (manual)
56
62
 
57
63
  Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Windows, `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
58
64
 
@@ -77,13 +83,14 @@ Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Window
77
83
  | `shift-cli start` | Start the daemon and configure the project |
78
84
  | `shift-cli init` | Scan and index project files |
79
85
  | `shift-cli update-drg` | Update the dependency relationship graph |
86
+ | `shift-cli add <tool>` | Add Shift MCP server to an AI coding tool |
80
87
  | `shift-cli stop` | Stop the daemon |
81
88
  | `shift-cli status` | Show current status |
82
89
  | `shift-cli config` | Manage configuration |
83
90
 
84
91
  ### `shift-cli init`
85
92
 
86
- Performs a full project scan — collects the file tree, categorizes files (source, config, assets), gathers git info, and sends everything to the Shift backend for indexing. Automatically starts the daemon if not running and creates a default `.shiftignore` if one doesn't exist.
93
+ Performs a full project scan — collects the file tree, categorizes files (source, config, assets), gathers git info, and sends everything to the Shift backend for indexing. Automatically starts the daemon if not running.
87
94
 
88
95
  If the project is already indexed, you will be prompted to re-index. Use `--force` to skip the prompt.
89
96
 
@@ -95,18 +102,16 @@ If the project is already indexed, you will be prompted to re-index. Use `--forc
95
102
  | `--project-id <id>` | Use existing project UUID |
96
103
  | `--template <id>` | Migration template ID for project creation |
97
104
  | `-f, --force` | Force re-indexing even if already indexed |
98
- | `--no-ignore` | Skip `.shiftignore` rules for this run (send all files) |
99
105
 
100
106
  ```bash
101
107
  shift-cli init # Interactive initialization
102
108
  shift-cli init --force # Force re-index without prompt
103
- shift-cli init --no-ignore # Index all files, skip .shiftignore
104
109
  shift-cli init --guest # Quick init with guest auth
105
110
  ```
106
111
 
107
112
  ### `shift-cli start`
108
113
 
109
- Resolves authentication, configures the project, and launches a background daemon that maintains a WebSocket connection to the Shift backend. Creates a default `.shiftignore` if one doesn't exist.
114
+ Resolves authentication, configures the project, and launches a background daemon that maintains a WebSocket connection to the Shift backend.
110
115
 
111
116
  | Flag | Description |
112
117
  |------|-------------|
@@ -124,12 +129,11 @@ shift-cli start --api-key <key> --project-name "My App"
124
129
 
125
130
  ### `shift-cli update-drg`
126
131
 
127
- Scans JavaScript/TypeScript files (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`), detects git changes, and sends file contents to the backend for dependency analysis. Respects `.shiftignore` rules.
132
+ Scans JavaScript/TypeScript files (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`), detects git changes, and sends file contents to the backend for dependency analysis.
128
133
 
129
134
  | Flag | Description |
130
135
  |------|-------------|
131
136
  | `-m, --mode <mode>` | `baseline` (all files) or `incremental` (git-changed only, default) |
132
- | `--no-ignore` | Skip `.shiftignore` rules for this run (send all files) |
133
137
 
134
138
  **Modes:**
135
139
  - **incremental** (default) — sends only git-changed files for faster updates
@@ -138,7 +142,18 @@ Scans JavaScript/TypeScript files (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`)
138
142
  ```bash
139
143
  shift-cli update-drg # Incremental update (default)
140
144
  shift-cli update-drg -m baseline # Full re-analysis
141
- shift-cli update-drg --no-ignore # Include ignored files
145
+ ```
146
+
147
+ ### `shift-cli add <tool>`
148
+
149
+ Configures Shift's MCP server in the specified AI coding tool. Reads `project_id` from `.shift/config.json` (run `shift-cli init` first).
150
+
151
+ ```bash
152
+ shift-cli add claude-code # Configure via Claude Code CLI
153
+ shift-cli add opencode # Write to opencode.json
154
+ shift-cli add codex # Configure via Codex CLI
155
+ shift-cli add copilot # Write to .vscode/mcp.json
156
+ shift-cli add droid # Configure via Droid CLI
142
157
  ```
143
158
 
144
159
  ### `shift-cli config`
@@ -0,0 +1,209 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { getProjectId } from '../../utils/config.js';
6
+ const execAsync = promisify(exec);
7
+ const SUPPORTED_TOOLS = ['claude-code', 'opencode', 'codex', 'copilot', 'droid'];
8
+ function isNotFound(e) {
9
+ return e.code === 'ENOENT' || /ENOENT|not found|not recognized/.test(e.message);
10
+ }
11
+ function getStderr(e) {
12
+ return (e.stderr || e.message || '').trim();
13
+ }
14
+ function shellEscape(arg) {
15
+ if (/^[a-zA-Z0-9._\-=/]+$/.test(arg))
16
+ return arg;
17
+ return `"${arg.replace(/"/g, '\\"')}"`;
18
+ }
19
+ function buildCommand(bin, args) {
20
+ return [bin, ...args.map(shellEscape)].join(' ');
21
+ }
22
+ function getMcpConfig(projectId) {
23
+ return {
24
+ command: 'shift-cli',
25
+ args: ['mcp'],
26
+ env: { SHIFT_PROJECT_ID: projectId },
27
+ };
28
+ }
29
+ async function addClaudeCode(projectId) {
30
+ const jsonPayload = JSON.stringify({
31
+ type: 'stdio',
32
+ command: 'shift-cli',
33
+ args: ['mcp'],
34
+ env: { SHIFT_PROJECT_ID: projectId },
35
+ });
36
+ try {
37
+ await execAsync(buildCommand('claude', ['mcp', 'add-json', 'shift', jsonPayload]));
38
+ console.log(' Added Shift MCP server to Claude Code.');
39
+ console.log('\n Verify with: claude mcp list');
40
+ }
41
+ catch (e) {
42
+ const err = e;
43
+ const stderr = getStderr(err);
44
+ if (/already exists/.test(stderr)) {
45
+ console.log(' Shift MCP server is already configured in Claude Code.');
46
+ console.log(' To update, remove it first: claude mcp remove shift');
47
+ }
48
+ else if (isNotFound(err)) {
49
+ console.log(' "claude" CLI not found. Add manually:\n');
50
+ console.log(` claude mcp add-json shift '${jsonPayload}'`);
51
+ }
52
+ else {
53
+ console.log(` Failed: ${stderr}`);
54
+ console.log('\n Try adding manually:\n');
55
+ console.log(` claude mcp add-json shift '${jsonPayload}'`);
56
+ }
57
+ }
58
+ }
59
+ async function addCodex(projectId) {
60
+ const config = getMcpConfig(projectId);
61
+ const args = ['mcp', 'add', 'shift'];
62
+ for (const [k, v] of Object.entries(config.env)) {
63
+ args.push('--env', `${k}=${v}`);
64
+ }
65
+ args.push('--', config.command, ...config.args);
66
+ const manualCmd = `codex ${args.join(' ')}`;
67
+ try {
68
+ await execAsync(buildCommand('codex', args));
69
+ console.log(' Added Shift MCP server to Codex.');
70
+ console.log('\n Verify with: codex mcp list');
71
+ }
72
+ catch (e) {
73
+ const err = e;
74
+ const stderr = getStderr(err);
75
+ if (isNotFound(err)) {
76
+ console.log(' "codex" CLI not found. Make sure Codex CLI is installed and on your PATH.\n');
77
+ console.log(' Once installed, add manually:\n');
78
+ }
79
+ else if (/already exists/.test(stderr)) {
80
+ console.log(' Shift MCP server is already configured in Codex.');
81
+ console.log(' To update, remove it first: codex mcp remove shift');
82
+ return;
83
+ }
84
+ else {
85
+ console.log(` Failed: ${stderr}`);
86
+ console.log('\n Try adding manually:\n');
87
+ }
88
+ console.log(` ${manualCmd}`);
89
+ }
90
+ }
91
+ async function addFactoryDroid(projectId) {
92
+ const config = getMcpConfig(projectId);
93
+ const args = ['mcp', 'add', 'shift', config.command];
94
+ for (const [k, v] of Object.entries(config.env)) {
95
+ args.push('--env', `${k}=${v}`);
96
+ }
97
+ const manualCmd = `droid ${args.join(' ')}`;
98
+ try {
99
+ await execAsync(buildCommand('droid', args));
100
+ console.log(' Added Shift MCP server to Factory Droid.');
101
+ console.log('\n Verify: type /mcp within droid to see configured servers');
102
+ }
103
+ catch (e) {
104
+ const err = e;
105
+ console.log(` Failed: ${getStderr(err)}`);
106
+ console.log('\n Try adding manually:\n');
107
+ console.log(` ${manualCmd}`);
108
+ }
109
+ }
110
+ function mergeJsonConfig(filePath, serverKey, serverValue) {
111
+ let config = {};
112
+ if (fs.existsSync(filePath)) {
113
+ try {
114
+ config = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
115
+ }
116
+ catch {
117
+ console.log(` Warning: Could not parse existing ${filePath}, creating new file.`);
118
+ config = {};
119
+ }
120
+ }
121
+ // Navigate/create nested keys
122
+ let obj = config;
123
+ for (let i = 0; i < serverKey.length - 1; i++) {
124
+ const key = serverKey[i];
125
+ if (typeof obj[key] !== 'object' || obj[key] === null) {
126
+ obj[key] = {};
127
+ }
128
+ obj = obj[key];
129
+ }
130
+ const finalKey = serverKey[serverKey.length - 1];
131
+ obj[finalKey] = serverValue;
132
+ // Ensure parent directory exists
133
+ const dir = path.dirname(filePath);
134
+ if (!fs.existsSync(dir)) {
135
+ fs.mkdirSync(dir, { recursive: true });
136
+ }
137
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
138
+ }
139
+ function addOpencode(projectId, projectRoot) {
140
+ const filePath = path.join(projectRoot, 'opencode.json');
141
+ let existing = {};
142
+ if (fs.existsSync(filePath)) {
143
+ try {
144
+ existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
145
+ }
146
+ catch { /* will be recreated */ }
147
+ }
148
+ if (!existing['$schema']) {
149
+ existing['$schema'] = 'https://opencode.ai/config.json';
150
+ const dir = path.dirname(filePath);
151
+ if (!fs.existsSync(dir))
152
+ fs.mkdirSync(dir, { recursive: true });
153
+ fs.writeFileSync(filePath, JSON.stringify(existing, null, 2) + '\n');
154
+ }
155
+ mergeJsonConfig(filePath, ['mcp', 'shift'], {
156
+ type: 'local',
157
+ command: ["shift-cli", "mcp"],
158
+ enabled: true,
159
+ environment: { "SHIFT_PROJECT_ID": projectId },
160
+ });
161
+ console.log(` Updated ${filePath}`);
162
+ console.log('\n Verify: check "mcp.shift" in opencode.json');
163
+ }
164
+ function addCopilot(projectId, projectRoot) {
165
+ const config = getMcpConfig(projectId);
166
+ const filePath = path.join(projectRoot, '.vscode', 'mcp.json');
167
+ mergeJsonConfig(filePath, ['servers', 'shift'], {
168
+ command: config.command,
169
+ args: config.args,
170
+ env: config.env,
171
+ });
172
+ console.log(` Updated ${filePath}`);
173
+ console.log('\n Verify: check "servers.shift" in .vscode/mcp.json');
174
+ }
175
+ export async function addCommand(tool) {
176
+ if (!SUPPORTED_TOOLS.includes(tool)) {
177
+ console.error(`\n Unknown tool: "${tool}"\n`);
178
+ console.error(' Supported tools:');
179
+ for (const t of SUPPORTED_TOOLS) {
180
+ console.error(` - ${t}`);
181
+ }
182
+ process.exit(1);
183
+ }
184
+ const projectRoot = process.cwd();
185
+ const projectId = getProjectId(projectRoot);
186
+ if (!projectId) {
187
+ console.error('\n No project configured. Run "shift-cli init" first to set up your project.\n');
188
+ process.exit(1);
189
+ }
190
+ console.log(`\n Configuring Shift MCP for ${tool}...\n`);
191
+ switch (tool) {
192
+ case 'claude-code':
193
+ await addClaudeCode(projectId);
194
+ break;
195
+ case 'opencode':
196
+ addOpencode(projectId, projectRoot);
197
+ break;
198
+ case 'codex':
199
+ await addCodex(projectId);
200
+ break;
201
+ case 'copilot':
202
+ addCopilot(projectId, projectRoot);
203
+ break;
204
+ case 'droid':
205
+ await addFactoryDroid(projectId);
206
+ break;
207
+ }
208
+ console.log('');
209
+ }
@@ -2,12 +2,13 @@ import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import { createInterface } from 'readline';
4
4
  import { createRequire } from 'module';
5
- import { readProjectConfig, writeProjectConfig } from '../../utils/config.js';
5
+ import { existsSync, readFileSync } from 'fs';
6
+ import path from 'path';
7
+ import { readProjectConfig, writeProjectConfig, ensureScanTargetFile } from '../../utils/config.js';
6
8
  import { fetchProjectStatus, sendInitScan } from '../../utils/api-client.js';
7
9
  import { resolveApiKey, resolveProject } from '../../utils/auth-resolver.js';
8
10
  import { getDaemonStatus, startDaemon } from '../../daemon/daemon-manager.js';
9
- import { getProjectTree, extractAllFilePaths, categorizeFiles } from '../../utils/tree-scanner.js';
10
- import { loadShiftIgnore, scaffoldShiftIgnore } from '../../utils/shiftignore.js';
11
+ import { getProjectTree, extractAllFilePaths, categorizeFiles, countProjectLOC } from '../../utils/tree-scanner.js';
11
12
  const require = createRequire(import.meta.url);
12
13
  const { version } = require('../../../package.json');
13
14
  const execAsync = promisify(exec);
@@ -64,6 +65,8 @@ export async function initCommand(options = {}) {
64
65
  console.error('\n❌ Failed to configure project.');
65
66
  process.exit(1);
66
67
  }
68
+ // Ensure .shift/scan_target.json exists (template for user customization)
69
+ ensureScanTargetFile(projectRoot);
67
70
  // Check if project is already indexed (skip check if --force flag is used)
68
71
  if (!options.force) {
69
72
  try {
@@ -121,18 +124,6 @@ export async function initCommand(options = {}) {
121
124
  }
122
125
  // Step 4: Scan project structure (matching extension's Step 6)
123
126
  console.log('[Init] Step 4/5: Scanning project structure...');
124
- // Scaffold .shiftignore if it doesn't exist
125
- if (scaffoldShiftIgnore(projectRoot)) {
126
- console.log('[Init] ✓ Created default .shiftignore (edit it to customize ignored files)\n');
127
- }
128
- // Load .shiftignore (skip if --no-ignore)
129
- const shiftIgnore = options.noIgnore ? null : loadShiftIgnore(projectRoot);
130
- if (options.noIgnore) {
131
- console.log('[Init] ⚠️ --no-ignore flag set — skipping .shiftignore rules\n');
132
- }
133
- else if (shiftIgnore) {
134
- console.log('[Init] ✓ Applying .shiftignore rules\n');
135
- }
136
127
  // Get project tree (matching extension's getProjectTree)
137
128
  const treeData = getProjectTree(projectRoot, {
138
129
  depth: 0, // Unlimited depth
@@ -149,7 +140,6 @@ export async function initCommand(options = {}) {
149
140
  'venv',
150
141
  'env',
151
142
  ],
152
- shiftIgnore,
153
143
  });
154
144
  console.log(`[Init] Files: ${treeData.file_count}`);
155
145
  console.log(`[Init] Directories: ${treeData.dir_count}`);
@@ -174,11 +164,49 @@ export async function initCommand(options = {}) {
174
164
  // Extract file paths and categorize (matching extension)
175
165
  const allFiles = extractAllFilePaths(treeData.tree);
176
166
  const categorized = categorizeFiles(treeData.tree);
167
+ // Count total lines of code
168
+ const totalLOC = countProjectLOC(projectRoot, allFiles);
169
+ console.log(`[Init] Total LOC: ${totalLOC}`);
177
170
  console.log(`[Init] Source files: ${categorized.source_files.length}`);
178
171
  console.log(`[Init] Config files: ${categorized.config_files.length}`);
179
172
  console.log(`[Init] Asset files: ${categorized.asset_files.length}\n`);
180
173
  // Step 5: Send scan to backend (matching extension's Step 9)
181
174
  console.log('[Init] Step 5/5: Sending scan to backend...');
175
+ // Read scan targets from .shift/scan_target.json if it exists
176
+ let scanTargets = null;
177
+ const scanTargetPath = path.join(projectRoot, '.shift', 'scan_target.json');
178
+ if (existsSync(scanTargetPath)) {
179
+ try {
180
+ const raw = readFileSync(scanTargetPath, 'utf-8');
181
+ const parsed = JSON.parse(raw);
182
+ // Accept both array format and single object format
183
+ const targets = Array.isArray(parsed) ? parsed : [parsed];
184
+ const mapped = targets.map((t) => ({
185
+ language: t.language ?? t.lang ?? null,
186
+ path: t.path ?? '',
187
+ }));
188
+ // Treat default template (single entry with language=null, path="") as unconfigured
189
+ const isDefault = mapped.length === 1 && mapped[0].language === null && mapped[0].path === '';
190
+ if (isDefault) {
191
+ console.log('[Init] .shift/scan_target.json is default template — server will auto-detect scan targets');
192
+ console.log('[Init] Edit the file to manually specify scan targets');
193
+ }
194
+ else {
195
+ scanTargets = mapped;
196
+ console.log(`[Init] ✓ Loaded ${scanTargets.length} scan target(s) from .shift/scan_target.json`);
197
+ for (const t of scanTargets) {
198
+ console.log(`[Init] → language=${t.language ?? 'auto'}, path="${t.path || '(root)'}"`);
199
+ }
200
+ }
201
+ }
202
+ catch (err) {
203
+ console.log(`[Init] ⚠️ Failed to read .shift/scan_target.json: ${err.message}`);
204
+ console.log('[Init] Proceeding without scan targets (server will auto-detect)');
205
+ }
206
+ }
207
+ else {
208
+ console.log('[Init] No .shift/scan_target.json found — server will auto-detect scan targets');
209
+ }
182
210
  const payload = {
183
211
  project_id: project.projectId,
184
212
  project_tree: treeData,
@@ -192,6 +220,8 @@ export async function initCommand(options = {}) {
192
220
  scan_timestamp: new Date().toISOString(),
193
221
  project_name: project.projectName,
194
222
  },
223
+ scan_targets: scanTargets,
224
+ total_loc: totalLOC,
195
225
  };
196
226
  try {
197
227
  const response = await sendInitScan(apiKey, project.projectId, payload);
@@ -1,7 +1,6 @@
1
- import { readProjectConfig, setProject } from '../../utils/config.js';
1
+ import { readProjectConfig, setProject, ensureScanTargetFile } from '../../utils/config.js';
2
2
  import { startDaemon, getDaemonStatus } from '../../daemon/daemon-manager.js';
3
3
  import { resolveApiKey, resolveProject } from '../../utils/auth-resolver.js';
4
- import { scaffoldShiftIgnore } from '../../utils/shiftignore.js';
5
4
  export async function startCommand(options = {}) {
6
5
  const projectRoot = process.cwd();
7
6
  const isAuthInteractive = !options.guest && !options.apiKey;
@@ -38,10 +37,8 @@ export async function startCommand(options = {}) {
38
37
  console.error('❌ Failed to configure project');
39
38
  process.exit(1);
40
39
  }
41
- // Scaffold .shiftignore if it doesn't exist
42
- if (scaffoldShiftIgnore(projectRoot)) {
43
- console.log('[Start] ✓ Created default .shiftignore (edit it to customize ignored files)\n');
44
- }
40
+ // Ensure .shift/scan_target.json exists (template for user customization)
41
+ ensureScanTargetFile(projectRoot);
45
42
  // Step 3: Check if daemon is already running
46
43
  console.log('[Start] Step 3/4: Checking daemon status...');
47
44
  const status = getDaemonStatus(projectRoot);
@@ -4,13 +4,12 @@ import { exec } from 'child_process';
4
4
  import { promisify } from 'util';
5
5
  import { getApiKey, readProjectConfig } from '../../utils/config.js';
6
6
  import { getProjectTree, extractAllFilePaths } from '../../utils/tree-scanner.js';
7
- import { loadShiftIgnore, filterPaths } from '../../utils/shiftignore.js';
8
7
  import { sendUpdateDrg } from '../../utils/api-client.js';
9
8
  const execAsync = promisify(exec);
10
9
  async function detectGitChanges(projectRoot, extensions) {
11
10
  const changes = { added: [], modified: [], deleted: [] };
12
11
  try {
13
- const { stdout } = await execAsync('git status --porcelain', { cwd: projectRoot });
12
+ const { stdout } = await execAsync('git status --porcelain -u', { cwd: projectRoot });
14
13
  for (const line of stdout.split('\n')) {
15
14
  if (!line.trim())
16
15
  continue;
@@ -52,7 +51,24 @@ async function detectGitChanges(projectRoot, extensions) {
52
51
  }
53
52
  return changes;
54
53
  }
55
- const JS_TS_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
54
+ const LANGUAGE_CONFIG = {
55
+ js_ts: {
56
+ extensions: new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']),
57
+ label: 'JS/TS',
58
+ },
59
+ python: {
60
+ extensions: new Set(['.py']),
61
+ label: 'Python',
62
+ },
63
+ csharp: {
64
+ extensions: new Set(['.cs']),
65
+ label: 'C#',
66
+ },
67
+ cpp: {
68
+ extensions: new Set(['.cpp', '.cc', '.cxx', '.h', '.hpp', '.c']),
69
+ label: 'C/C++',
70
+ },
71
+ };
56
72
  export async function updateDrgCommand(options = {}) {
57
73
  const projectRoot = process.cwd();
58
74
  const mode = options.mode || 'incremental';
@@ -76,15 +92,8 @@ export async function updateDrgCommand(options = {}) {
76
92
  process.exit(1);
77
93
  }
78
94
  console.log(`[DRG] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
79
- // Step 3: Scan project for JS/TS files
80
- console.log('[DRG] Step 3/4: Scanning project for JS/TS files...');
81
- const shiftIgnore = options.noIgnore ? null : loadShiftIgnore(projectRoot);
82
- if (options.noIgnore) {
83
- console.log('[DRG] ⚠️ --no-ignore flag set — skipping .shiftignore rules\n');
84
- }
85
- else if (shiftIgnore) {
86
- console.log('[DRG] ✓ Found .shiftignore — applying custom ignore rules\n');
87
- }
95
+ // Step 3: Scan project for all supported language files
96
+ console.log('[DRG] Step 3/4: Scanning project for supported languages...');
88
97
  const treeData = getProjectTree(projectRoot, {
89
98
  depth: 0,
90
99
  exclude_patterns: [
@@ -100,89 +109,86 @@ export async function updateDrgCommand(options = {}) {
100
109
  'venv',
101
110
  'env',
102
111
  ],
103
- shiftIgnore,
104
112
  });
105
113
  const allFiles = extractAllFilePaths(treeData.tree);
106
- const jstsFiles = allFiles.filter((filePath) => {
107
- const ext = path.extname(filePath).toLowerCase();
108
- return JS_TS_EXTENSIONS.has(ext);
109
- });
114
+ // Group files by language (each file counted in first matching language)
115
+ const filesByLanguage = {};
116
+ for (const lang of Object.keys(LANGUAGE_CONFIG)) {
117
+ const extSet = LANGUAGE_CONFIG[lang].extensions;
118
+ filesByLanguage[lang] = allFiles.filter((filePath) => extSet.has(path.extname(filePath).toLowerCase()));
119
+ }
120
+ const languagesWithFiles = Object.entries(filesByLanguage).filter(([_, files]) => files.length > 0);
110
121
  console.log(`[DRG] Total files scanned: ${allFiles.length}`);
111
- console.log(`[DRG] JS/TS files found: ${jstsFiles.length}\n`);
112
- if (jstsFiles.length === 0) {
113
- console.log('⚠️ No JS/TS files found in project. Nothing to update.\n');
122
+ for (const [lang, files] of languagesWithFiles) {
123
+ console.log(`[DRG] ${LANGUAGE_CONFIG[lang].label} files: ${files.length}`);
124
+ }
125
+ console.log('');
126
+ if (languagesWithFiles.length === 0) {
127
+ console.log('⚠️ No supported language files found (JS/TS, Python, C#, C++). Nothing to update.\n');
114
128
  return;
115
129
  }
116
- // Read file contents
117
- const files = [];
118
- let readErrors = 0;
119
- for (const filePath of jstsFiles) {
120
- const absolutePath = path.join(projectRoot, filePath);
121
- try {
122
- const content = fs.readFileSync(absolutePath, 'utf-8');
123
- files.push({ file_path: filePath, content });
130
+ let anySent = false;
131
+ let lastError = null;
132
+ for (const [language, langFiles] of languagesWithFiles) {
133
+ const extSet = LANGUAGE_CONFIG[language].extensions;
134
+ const label = LANGUAGE_CONFIG[language].label;
135
+ // Read file contents for this language
136
+ const files = [];
137
+ let readErrors = 0;
138
+ for (const filePath of langFiles) {
139
+ const absolutePath = path.join(projectRoot, filePath);
140
+ try {
141
+ const content = fs.readFileSync(absolutePath, 'utf-8');
142
+ files.push({ file_path: filePath, content });
143
+ }
144
+ catch {
145
+ readErrors++;
146
+ }
124
147
  }
125
- catch {
126
- readErrors++;
148
+ if (readErrors > 0) {
149
+ console.log(`[DRG] [${label}] ⚠️ Could not read ${readErrors} file(s)\n`);
127
150
  }
128
- }
129
- if (readErrors > 0) {
130
- console.log(`[DRG] ⚠️ Could not read ${readErrors} file(s)\n`);
131
- }
132
- // Always use git to compute added/modified/deleted (single source of truth)
133
- console.log('[DRG] Detecting git changes...');
134
- const gitChanges = await detectGitChanges(projectRoot, JS_TS_EXTENSIONS);
135
- // Filter git changes through .shiftignore
136
- gitChanges.added = filterPaths(shiftIgnore, gitChanges.added);
137
- gitChanges.modified = filterPaths(shiftIgnore, gitChanges.modified);
138
- gitChanges.deleted = filterPaths(shiftIgnore, gitChanges.deleted);
139
- console.log(`[DRG] Git changes added: ${gitChanges.added.length}, modified: ${gitChanges.modified.length}, deleted: ${gitChanges.deleted.length}`);
140
- // Which files to send: incremental = only changed; baseline = all (full re-analysis)
141
- const changedPaths = new Set([...gitChanges.added, ...gitChanges.modified]);
142
- const filesToSend = mode === 'baseline'
143
- ? files
144
- : files.filter((f) => changedPaths.has(f.file_path));
145
- if (mode === 'baseline') {
146
- console.log(`[DRG] Baseline: sending all ${filesToSend.length} JS/TS files.\n`);
147
- }
148
- else {
149
- console.log(`[DRG] Incremental: sending ${filesToSend.length} changed file(s).\n`);
150
- }
151
- if (filesToSend.length === 0 && gitChanges.deleted.length === 0) {
152
- console.log('\n✓ No JS/TS changes detected. DRG is up to date.\n');
153
- return;
154
- }
155
- // Step 4: Send to backend
156
- console.log(`[DRG] Step 4/4: Sending ${filesToSend.length} files to backend (mode: ${mode})...`);
157
- const payload = {
158
- project_id: projectConfig.project_id,
159
- language: 'js_ts',
160
- mode: mode,
161
- changes: gitChanges,
162
- files: filesToSend,
163
- };
164
- try {
165
- const response = await sendUpdateDrg(apiKey, payload);
166
- console.log('\n╔═══════════════════════════════════════════════╗');
167
- console.log('║ ✓ DRG Update Complete ║');
168
- console.log('╚═══════════════════════════════════════════════╝\n');
169
- console.log(` Success: ${response.success}`);
170
- console.log(` Message: ${response.message}`);
171
- if (response.stats) {
172
- console.log(` Files sent: ${response.stats.total_files_provided}`);
173
- console.log(` Unique: ${response.stats.unique_paths}`);
174
- console.log(` Added: ${response.stats.added}`);
175
- console.log(` Modified: ${response.stats.modified}`);
176
- console.log(` Deleted: ${response.stats.deleted}`);
177
- const affected = response.stats.affected_file_paths;
178
- if (affected?.length) {
179
- console.log(` Affected by deletions (edges updated): ${affected.length} file(s)`);
151
+ const gitChanges = await detectGitChanges(projectRoot, extSet);
152
+ const changedPaths = new Set([...gitChanges.added, ...gitChanges.modified]);
153
+ const filesToSend = mode === 'baseline'
154
+ ? files
155
+ : files.filter((f) => changedPaths.has(f.file_path));
156
+ if (filesToSend.length === 0 && gitChanges.deleted.length === 0) {
157
+ console.log(`[DRG] [${label}] No changes. Skipping.\n`);
158
+ continue;
159
+ }
160
+ console.log(`[DRG] [${label}] Git changes — added: ${gitChanges.added.length}, modified: ${gitChanges.modified.length}, deleted: ${gitChanges.deleted.length}`);
161
+ console.log(`[DRG] [${label}] Sending ${filesToSend.length} file(s) (mode: ${mode})...`);
162
+ const payload = {
163
+ project_id: projectConfig.project_id,
164
+ language,
165
+ mode,
166
+ changes: gitChanges,
167
+ files: filesToSend,
168
+ };
169
+ try {
170
+ const response = await sendUpdateDrg(apiKey, payload);
171
+ anySent = true;
172
+ console.log(`[DRG] [${label}] ${response.message}`);
173
+ if (response.stats) {
174
+ console.log(`[DRG] [${label}] Files sent: ${response.stats.total_files_provided}, Added: ${response.stats.added}, Modified: ${response.stats.modified}, Deleted: ${response.stats.deleted}`);
180
175
  }
181
176
  }
182
- console.log('\nUse "shift-cli status" to check progress.\n');
177
+ catch (error) {
178
+ lastError = error;
179
+ console.error(`[DRG] [${label}] ❌ ${error.message}`);
180
+ }
181
+ console.log('');
183
182
  }
184
- catch (error) {
185
- console.error(`\n❌ Failed to update DRG: ${error.message}`);
183
+ // Step 4: Summary
184
+ console.log('╔═══════════════════════════════════════════════╗');
185
+ console.log('║ ✓ DRG Update Complete ║');
186
+ console.log('╚═══════════════════════════════════════════════╝\n');
187
+ if (lastError && !anySent) {
188
+ console.error(`❌ All updates failed. Last error: ${lastError.message}`);
186
189
  process.exit(1);
187
190
  }
191
+ if (lastError) {
192
+ console.log(`⚠️ Some languages failed (see above).`);
193
+ }
188
194
  }
package/build/index.js CHANGED
@@ -16,8 +16,7 @@ program
16
16
  ' shift-cli status Check current status\n\n' +
17
17
  'Configuration:\n' +
18
18
  ' Global config: ~/.shift/config.json\n' +
19
- ' Project config: .shift/config.json\n' +
20
- ' Ignore rules: .shiftignore (auto-created on first run)')
19
+ ' Project config: .shift/config.json')
21
20
  .version(version);
22
21
  // MCP server mode (default when run via MCP host)
23
22
  program
@@ -55,7 +54,7 @@ startCmd.addHelpText('after', `
55
54
  Details:
56
55
  Resolves authentication, configures the project, and launches a
57
56
  background daemon that maintains a WebSocket connection to the
58
- Shift backend. Creates a default .shiftignore if one doesn't exist.
57
+ Shift backend.
59
58
 
60
59
  Examples:
61
60
  shift-cli start Interactive setup
@@ -72,7 +71,6 @@ const initCmd = program
72
71
  .option('--project-name <name>', 'Create a new project or match an existing one by name')
73
72
  .option('--project-id <id>', 'Link to an existing project by its UUID')
74
73
  .option('--template <id>', 'Use a specific migration template when creating the project')
75
- .option('--no-ignore', 'Skip .shiftignore rules for this run (send all files)')
76
74
  .action(async (options) => {
77
75
  const { initCommand } = await import('./cli/commands/init.js');
78
76
  await initCommand({
@@ -82,7 +80,6 @@ const initCmd = program
82
80
  projectName: options.projectName,
83
81
  projectId: options.projectId,
84
82
  template: options.template,
85
- noIgnore: options.noIgnore,
86
83
  });
87
84
  });
88
85
  initCmd.addHelpText('after', `
@@ -90,7 +87,6 @@ Details:
90
87
  Performs a full project scan — collects the file tree, categorizes files
91
88
  (source, config, assets), gathers git info, and sends everything to the
92
89
  Shift backend for indexing. Automatically starts the daemon if not running.
93
- Creates a default .shiftignore if one doesn't exist.
94
90
 
95
91
  If the project is already indexed, you will be prompted to re-index.
96
92
  Use --force to skip the prompt.
@@ -98,7 +94,6 @@ Details:
98
94
  Examples:
99
95
  shift-cli init Interactive initialization
100
96
  shift-cli init --force Force re-index without prompt
101
- shift-cli init --no-ignore Index all files, ignore .shiftignore
102
97
  shift-cli init --guest Quick init with guest auth
103
98
  `);
104
99
  // --- stop ---
@@ -136,16 +131,15 @@ const updateDrgCmd = program
136
131
  .command('update-drg')
137
132
  .description('Update the dependency relationship graph (DRG)')
138
133
  .option('-m, --mode <mode>', 'Update mode: "baseline" (all files) or "incremental" (git-changed only)', 'incremental')
139
- .option('--no-ignore', 'Skip .shiftignore rules for this run (send all files)')
140
134
  .action(async (options) => {
141
135
  const { updateDrgCommand } = await import('./cli/commands/update-drg.js');
142
- await updateDrgCommand({ mode: options.mode, noIgnore: options.noIgnore });
136
+ await updateDrgCommand({ mode: options.mode });
143
137
  });
144
138
  updateDrgCmd.addHelpText('after', `
145
139
  Details:
146
140
  Scans JavaScript/TypeScript files (.js, .jsx, .ts, .tsx, .mjs, .cjs),
147
141
  detects git changes, and sends file contents to the backend for
148
- dependency analysis. Respects .shiftignore rules.
142
+ dependency analysis.
149
143
 
150
144
  Modes:
151
145
  incremental Send only git-changed files (default, faster)
@@ -154,7 +148,27 @@ Modes:
154
148
  Examples:
155
149
  shift-cli update-drg Incremental update
156
150
  shift-cli update-drg -m baseline Full re-analysis
157
- shift-cli update-drg --no-ignore Include ignored files
151
+ `);
152
+ // --- add mcp servers---
153
+ const addCmd = program
154
+ .command('add <tool>')
155
+ .description('Add Shift MCP server to an AI coding tool')
156
+ .action(async (tool) => {
157
+ const { addCommand } = await import('./cli/commands/add.js');
158
+ await addCommand(tool);
159
+ });
160
+ addCmd.addHelpText('after', `
161
+ Supported tools:
162
+ claude-code Configure via Claude Code CLI
163
+ opencode Write to opencode.json
164
+ codex Configure via Codex CLI
165
+ copilot Write to .vscode/mcp.json
166
+ droid Configure via Factory-Droid CLI
167
+
168
+ Examples:
169
+ shift-cli add claude-code
170
+ shift-cli add copilot
171
+ shift-cli add opencode
158
172
  `);
159
173
  // --- config ---
160
174
  const configCmd = program
@@ -48,19 +48,28 @@ export async function fetchProjects(apiKey) {
48
48
  * Matching extension's init-scan API call
49
49
  */
50
50
  export async function sendInitScan(apiKey, projectId, payload) {
51
- const response = await fetch(`${API_BASE_URL_ORCH}/api/projects/${projectId}/init-scan`, {
52
- method: 'POST',
53
- headers: {
54
- 'Authorization': `Bearer ${apiKey}`,
55
- 'Content-Type': 'application/json',
56
- },
57
- body: JSON.stringify(payload),
58
- });
59
- if (!response.ok) {
60
- const text = await response.text();
61
- throw new Error(text || `HTTP ${response.status}`);
51
+ try {
52
+ // Ensure proper URL construction with leading slash
53
+ const path = `/api/projects/${projectId}/init-scan`;
54
+ const url = new URL(path, API_BASE_URL_ORCH).toString();
55
+ const response = await fetch(url, {
56
+ method: 'POST',
57
+ headers: {
58
+ 'Authorization': `Bearer ${apiKey}`,
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ body: JSON.stringify(payload),
62
+ });
63
+ if (!response.ok) {
64
+ const text = await response.text();
65
+ throw new Error(text || `HTTP ${response.status}`);
66
+ }
67
+ return await response.json();
68
+ }
69
+ catch (error) {
70
+ // Re-throw for caller to handle; keep stack/context
71
+ throw error;
62
72
  }
63
- return await response.json();
64
73
  }
65
74
  /**
66
75
  * Send update-drg request to backend
@@ -132,6 +132,22 @@ export function ensureLocalShiftDir(projectRoot) {
132
132
  fs.mkdirSync(dir, { recursive: true });
133
133
  }
134
134
  }
135
+ const SCAN_TARGET_TEMPLATE = [
136
+ {
137
+ language: null,
138
+ path: '',
139
+ },
140
+ ];
141
+ /**
142
+ * Create .shift/scan_target.json with a template if it doesn't exist.
143
+ * Users can edit this file to manually specify scan targets.
144
+ */
145
+ export function ensureScanTargetFile(projectRoot) {
146
+ const filePath = path.join(getLocalShiftDir(projectRoot), 'scan_target.json');
147
+ if (!fs.existsSync(filePath)) {
148
+ fs.writeFileSync(filePath, JSON.stringify(SCAN_TARGET_TEMPLATE, null, 2));
149
+ }
150
+ }
135
151
  export function readProjectConfig(projectRoot) {
136
152
  try {
137
153
  const filePath = path.join(getLocalShiftDir(projectRoot), LOCAL_CONFIG_FILE);
@@ -152,8 +168,10 @@ export function writeProjectConfig(config, projectRoot) {
152
168
  // Also create .gitignore in .shift folder (matching extension)
153
169
  const gitignorePath = path.join(getLocalShiftDir(projectRoot), '.gitignore');
154
170
  if (!fs.existsSync(gitignorePath)) {
155
- fs.writeFileSync(gitignorePath, '*\n!.gitignore\n!config.json\n');
171
+ fs.writeFileSync(gitignorePath, '*\n!.gitignore\n!config.json\n!scan_target.json\n');
156
172
  }
173
+ // Create scan_target.json template if it doesn't exist
174
+ ensureScanTargetFile(projectRoot);
157
175
  }
158
176
  export function getProjectId(projectRoot) {
159
177
  const config = readProjectConfig(projectRoot);
@@ -21,7 +21,7 @@ const DEFAULT_EXCLUDE_PATTERNS = [
21
21
  * Get project tree - matching extension's getProjectTree function
22
22
  */
23
23
  export function getProjectTree(workspaceRoot, options = {}) {
24
- const { depth = 0, exclude_patterns = DEFAULT_EXCLUDE_PATTERNS, shiftIgnore = null, } = options;
24
+ const { depth = 0, exclude_patterns = DEFAULT_EXCLUDE_PATTERNS, } = options;
25
25
  let file_count = 0;
26
26
  let dir_count = 0;
27
27
  let total_size = 0;
@@ -40,15 +40,6 @@ export function getProjectTree(workspaceRoot, options = {}) {
40
40
  }
41
41
  const itemPath = path.join(dirPath, entry.name);
42
42
  const itemRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
43
- // Check if should be excluded by .shiftignore
44
- if (shiftIgnore) {
45
- // Use forward slashes for ignore matching; add trailing / for directories
46
- const testPath = itemRelativePath.replace(/\\/g, '/');
47
- const isDir = entry.isDirectory();
48
- if (shiftIgnore.ignores(isDir ? testPath + '/' : testPath)) {
49
- continue;
50
- }
51
- }
52
43
  if (entry.isDirectory()) {
53
44
  dir_count++;
54
45
  const children = scanDirectory(itemPath, currentDepth + 1, itemRelativePath);
@@ -123,6 +114,23 @@ export function extractAllFilePaths(tree, basePath = '') {
123
114
  * Categorize files by type
124
115
  * Matching extension's categorizeFiles function
125
116
  */
117
+ /**
118
+ * Count total lines of code across all project files
119
+ */
120
+ export function countProjectLOC(rootPath, filePaths) {
121
+ let totalLOC = 0;
122
+ for (const filePath of filePaths) {
123
+ try {
124
+ const fullPath = path.join(rootPath, filePath);
125
+ const content = fs.readFileSync(fullPath, 'utf-8');
126
+ totalLOC += content.split('\n').length;
127
+ }
128
+ catch {
129
+ // Skip binary/unreadable files
130
+ }
131
+ }
132
+ return totalLOC;
133
+ }
126
134
  export function categorizeFiles(tree, basePath = '') {
127
135
  const categories = {
128
136
  source_files: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latentforce/shift",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Shift CLI - AI-powered code intelligence with MCP support",
5
5
  "type": "module",
6
6
  "main": "./build/index.js",
@@ -38,7 +38,6 @@
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/sdk": "^1.25.3",
40
40
  "commander": "^12.0.0",
41
- "ignore": "^7.0.5",
42
41
  "ws": "^8.14.0",
43
42
  "zod": "^3.25.76"
44
43
  },