@latentforce/shift 1.0.10 → 1.0.12

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
@@ -10,65 +10,45 @@ Shift indexes your codebase, builds a dependency relationship graph (DRG), and p
10
10
  npm install -g @latentforce/shift
11
11
  ```
12
12
 
13
- Requires Node.js >= 18.
13
+ ## Getting an API Key
14
14
 
15
- ## Quick Start
15
+ 1. Go to **https://dev-shift-lite.latentforce.ai/auth**
16
+ 2. **Sign up** for an account
17
+ 3. **Sign in** to your dashboard
18
+ 4. Copy your **API key** from the dashboard
16
19
 
17
- ### Guest mode (zero-config)
20
+ From the dashboard you can also **create a project** and **select a migration template** — or you can do both directly from the CLI (see below).
18
21
 
19
- ```bash
20
- cd /path/to/your/project
21
- shift-cli init --guest # Auto-creates guest key + project, scans, and indexes
22
- ```
22
+ ## Quick Start
23
23
 
24
- ### With an API key
24
+ All commands **must be run** from your project's root directory:
25
25
 
26
26
  ```bash
27
- # Create a new project and index it (prompts for template selection)
28
- shift-cli init --api-key YOUR_KEY --project-name "My App"
29
-
30
- # Fully non-interactive (CI/CD friendly)
31
- shift-cli init --api-key YOUR_KEY --project-name "My App" --template TEMPLATE_ID
27
+ cd /path/to/your/project
32
28
  ```
33
29
 
34
- ### Interactive (original behavior)
30
+ ### Option A: Interactive (recommended)
35
31
 
36
32
  ```bash
37
- shift-cli start # Configure API key and project interactively
38
- shift-cli init # Index project files
33
+ shift-cli start # Step 1: Configure API key and project, launch daemon
34
+ shift-cli init # Step 2: Scan and index project files
35
+ shift-cli status # Step 3: Verify everything is connected
39
36
  ```
40
37
 
41
- ## MCP Integration
42
-
43
- ### Add to Claude Code
44
-
45
- ```bash
46
- claude mcp add-json shift '{"type":"stdio","command":"shift-cli","args":["mcp"],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID"}}'
47
- ```
38
+ When you run `shift-cli start` interactively, it will:
39
+ 1. **Ask for your API key** — paste the key from your dashboard (or choose guest mode)
40
+ 2. **Ask to create or select a project** — you can either:
41
+ - **Select an existing project** from your account (if you created one on the dashboard)
42
+ - **Create a new project** from the CLI — it will prompt for a name (defaults to your directory name) and let you select a migration template
48
43
 
49
- Or using npx (no global install needed):
44
+ ### Option B: Non-interactive (CI/CD friendly)
50
45
 
51
46
  ```bash
52
- claude mcp add-json shift '{"type":"stdio","command":"npx","args":["@latentforce/shift","mcp"],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID"}}'
47
+ shift-cli init --api-key YOUR_KEY --project-name "My App"
53
48
  ```
54
49
 
55
- ### Add to Claude Desktop
56
-
57
- Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Windows, `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
50
+ If a project named "My App" already exists in your account, it will be reused. Otherwise a new one is created with the specified template.
58
51
 
59
- ```json
60
- {
61
- "mcpServers": {
62
- "shift": {
63
- "command": "shift-cli",
64
- "args": ["mcp"],
65
- "env": {
66
- "SHIFT_PROJECT_ID": "YOUR_PROJECT_ID"
67
- }
68
- }
69
- }
70
- }
71
- ```
72
52
 
73
53
  ## CLI Commands
74
54
 
@@ -76,37 +56,44 @@ Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Window
76
56
  |---------|-------------|
77
57
  | `shift-cli start` | Start the daemon and configure the project |
78
58
  | `shift-cli init` | Scan and index project files |
59
+ | `shift-cli status` | Show current status |
79
60
  | `shift-cli update-drg` | Update the dependency relationship graph |
80
61
  | `shift-cli stop` | Stop the daemon |
81
- | `shift-cli status` | Show current status |
62
+ | `shift-cli add <tool>` | Add Shift MCP server to an AI coding tool |
82
63
  | `shift-cli config` | Manage configuration |
83
64
 
84
- ### `shift-cli init`
65
+ ### `shift-cli start`
85
66
 
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.
67
+ Resolves authentication, configures the project, and launches a background daemon that maintains a WebSocket connection to the Shift backend.
87
68
 
88
- If the project is already indexed, you will be prompted to re-index. Use `--force` to skip the prompt.
69
+ **When run interactively (no flags):**
70
+ 1. Prompts you to enter your API key or choose guest mode
71
+ 2. Prompts you to **select an existing project** or **create a new one**
72
+ - If creating: asks for a project name and lets you pick a migration template
73
+ 3. Saves config to `.shift/config.json`
74
+ 4. Launches the background daemon
75
+
76
+ If you already created a project on the dashboard (https://dev-shift-lite.latentforce.ai), you can select it from the list. Otherwise, create one directly from the CLI.
89
77
 
90
78
  | Flag | Description |
91
79
  |------|-------------|
92
80
  | `--guest` | Use guest authentication (auto-creates a temporary project) |
93
- | `--api-key <key>` | Provide API key directly |
94
- | `--project-name <name>` | Create new project or match existing by name |
95
- | `--project-id <id>` | Use existing project UUID |
81
+ | `--api-key <key>` | Provide API key directly (skips the key prompt) |
82
+ | `--project-name <name>` | Create new project or match existing by name (skips project prompt) |
83
+ | `--project-id <id>` | Use existing project UUID (skips project prompt) |
96
84
  | `--template <id>` | Migration template ID for project creation |
97
- | `-f, --force` | Force re-indexing even if already indexed |
98
- | `--no-ignore` | Skip `.shiftignore` rules for this run (send all files) |
99
85
 
100
86
  ```bash
101
- shift-cli init # Interactive initialization
102
- shift-cli init --force # Force re-index without prompt
103
- shift-cli init --no-ignore # Index all files, skip .shiftignore
104
- shift-cli init --guest # Quick init with guest auth
87
+ shift-cli start # Interactive setup (prompts for key + project)
88
+ shift-cli start --guest # Quick start without API key
89
+ shift-cli start --api-key <key> --project-name "My App" # Non-interactive
105
90
  ```
106
91
 
107
- ### `shift-cli start`
92
+ ### `shift-cli init`
93
+
94
+ 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.
108
95
 
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.
96
+ If the project is already indexed, you will be prompted to re-index. Use `--force` to skip the prompt.
110
97
 
111
98
  | Flag | Description |
112
99
  |------|-------------|
@@ -115,30 +102,53 @@ Resolves authentication, configures the project, and launches a background daemo
115
102
  | `--project-name <name>` | Create new project or match existing by name |
116
103
  | `--project-id <id>` | Use existing project UUID |
117
104
  | `--template <id>` | Migration template ID for project creation |
105
+ | `-f, --force` | Force re-indexing even if already indexed |
118
106
 
119
107
  ```bash
120
- shift-cli start # Interactive setup
121
- shift-cli start --guest # Quick start without API key
122
- shift-cli start --api-key <key> --project-name "My App"
108
+ shift-cli init # Interactive initialization
109
+ shift-cli init --force # Force re-index without prompt
110
+ ```
111
+
112
+ ### `shift-cli status`
113
+
114
+ Displays comprehensive information about the current Shift setup — API key status, project details, backend indexing state, and daemon health.
115
+
116
+ ```bash
117
+ shift-cli status
123
118
  ```
124
119
 
125
120
  ### `shift-cli update-drg`
126
121
 
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.
122
+ Scans source files across all supported languages, detects git changes, and sends file contents to the backend for dependency analysis.
123
+
124
+ **Supported languages and extensions:**
125
+
126
+ | Language | Extensions |
127
+ |----------|------------|
128
+ | JavaScript / TypeScript | `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs` |
129
+ | Python | `.py` |
130
+ | C# | `.cs` |
131
+ | C/C++ | `.cpp`, `.cc`, `.cxx`, `.h`, `.hpp`, `.c` |
128
132
 
129
133
  | Flag | Description |
130
134
  |------|-------------|
131
135
  | `-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
136
 
134
137
  **Modes:**
135
138
  - **incremental** (default) — sends only git-changed files for faster updates
136
- - **baseline** — sends all JS/TS files for a full re-analysis
139
+ - **baseline** — sends all source files for a full re-analysis
137
140
 
138
141
  ```bash
139
142
  shift-cli update-drg # Incremental update (default)
140
143
  shift-cli update-drg -m baseline # Full re-analysis
141
- shift-cli update-drg --no-ignore # Include ignored files
144
+ ```
145
+
146
+ ### `shift-cli stop`
147
+
148
+ Stops the background daemon process.
149
+
150
+ ```bash
151
+ shift-cli stop
142
152
  ```
143
153
 
144
154
  ### `shift-cli config`
@@ -151,92 +161,141 @@ Manage Shift configuration (URLs, API key).
151
161
  | `set <key> <value>` | Set a configuration value |
152
162
  | `clear [key]` | Clear a specific key or all configuration |
153
163
 
154
- **Configurable keys:** `api-key`, `api-url`, `orch-url`, `ws-url`
155
-
156
- URLs can also be set via environment variables: `SHIFT_API_URL`, `SHIFT_ORCH_URL`, `SHIFT_WS_URL`.
164
+ **Configurable key:** `api-key`
157
165
 
158
166
  ```bash
159
167
  shift-cli config # Show config
160
168
  shift-cli config set api-key sk-abc123 # Set API key
161
- shift-cli config set api-url https://api.shift.ai # Set API URL
162
169
  shift-cli config clear api-key # Clear API key
163
170
  shift-cli config clear # Clear all config
164
171
  ```
172
+ ## MCP Integration
173
+
174
+ After initializing your project, use `shift-cli add` to configure Shift's MCP server in your AI coding tool:
175
+
176
+ ```bash
177
+ shift-cli add <tool>
178
+ ```
179
+
180
+ ### Supported tools
181
+
182
+ | Tool | Command | Config method |
183
+ |------|---------|---------------|
184
+ | Claude Code | `shift-cli add claude-code` | Runs `claude mcp add-json` |
185
+ | Opencode | `shift-cli add opencode` | Writes `opencode.json` |
186
+ | Codex | `shift-cli add codex` | Runs `codex mcp add` |
187
+ | GitHub Copilot | `shift-cli add copilot` | Writes `.vscode/mcp.json` |
188
+ | Factory Droid | `shift-cli add droid` | Runs `droid mcp add` |
189
+
190
+ For CLI-based tools, if the CLI is not installed, the command will print manual setup instructions.
191
+
192
+ ### Claude Desktop (manual)
165
193
 
166
- ## `.shiftignore`
194
+ Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Windows, `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
195
+
196
+ ```json
197
+ {
198
+ "mcpServers": {
199
+ "shift": {
200
+ "command": "shift-cli",
201
+ "args": ["mcp"],
202
+ "env": {
203
+ "SHIFT_PROJECT_ID": "YOUR_PROJECT_ID"
204
+ }
205
+ }
206
+ }
207
+ }
208
+ ```
209
+ ## `.shift/scan_target.json`
167
210
 
168
- Shift uses a `.shiftignore` file to control which files and directories are excluded from indexing and dependency analysis. It follows the same syntax as `.gitignore`.
211
+ Shift uses a `scan_target.json` file inside the `.shift/` directory to let you specify which parts of your codebase to scan and in which language. This is especially useful for monorepos or projects with multiple languages.
169
212
 
170
- A default `.shiftignore` is **automatically created** the first time you run `shift-cli init` or `shift-cli start`. You can edit it to customize which files are excluded.
213
+ A default template is **automatically created** the first time you run `shift-cli init` or `shift-cli start`. By default, it is unconfigured and the server will auto-detect scan targets. Edit the file to manually specify targets.
171
214
 
172
- ### Syntax
215
+ ### Valid Languages
173
216
 
174
- - Blank lines are ignored
175
- - Lines starting with `#` are comments
176
- - Standard glob patterns (`*`, `**`, `?`)
177
- - Trailing `/` matches directories only
178
- - Leading `/` anchors to the project root
179
- - `!` negates a pattern (re-includes a previously ignored path)
217
+ ```
218
+ javascript, typescript, python, cpp, csharp
219
+ ```
180
220
 
181
- ### Default `.shiftignore`
221
+ ### Format
182
222
 
183
- The auto-generated file excludes common non-source directories and files:
223
+ The file is a JSON array of objects, each with:
184
224
 
185
- ```gitignore
186
- # Dependencies
187
- node_modules/
188
- vendor/
189
- bower_components/
225
+ | Field | Type | Description |
226
+ |-------|------|-------------|
227
+ | `language` | `string \| null` | One of the valid languages above, or `null` for auto-detect |
228
+ | `path` | `string` | Relative path from project root (empty string `""` for root) |
190
229
 
191
- # Build output
192
- dist/
193
- build/
194
- out/
195
- .next/
230
+ ### Default Template (auto-generated)
196
231
 
197
- # Test & coverage
198
- coverage/
199
- .nyc_output/
232
+ ```json
233
+ [
234
+ {
235
+ "language": null,
236
+ "path": ""
237
+ }
238
+ ]
239
+ ```
200
240
 
201
- # Environment & secrets
202
- .env
203
- .env.*
204
- *.pem
205
- *.key
241
+ When left as-is (single entry with `language: null` and `path: ""`), the server auto-detects scan targets.
206
242
 
207
- # Logs
208
- *.log
209
- logs/
243
+ ### Examples
210
244
 
211
- # OS files
212
- .DS_Store
213
- Thumbs.db
245
+ **Single-language project (TypeScript at root):**
214
246
 
215
- # IDE
216
- .vscode/
217
- .idea/
218
- *.swp
219
- *.swo
247
+ ```json
248
+ [
249
+ {
250
+ "language": "typescript",
251
+ "path": ""
252
+ }
253
+ ]
254
+ ```
220
255
 
221
- # Python
222
- __pycache__/
223
- *.pyc
224
- venv/
225
- .venv/
256
+ **Monorepo with multiple languages:**
226
257
 
227
- # Misc
228
- *.min.js
229
- *.min.css
230
- *.map
258
+ ```json
259
+ [
260
+ {
261
+ "language": "typescript",
262
+ "path": "frontend"
263
+ },
264
+ {
265
+ "language": "python",
266
+ "path": "backend"
267
+ },
268
+ {
269
+ "language": "csharp",
270
+ "path": "services/auth"
271
+ }
272
+ ]
231
273
  ```
232
274
 
233
- ### Bypassing `.shiftignore`
275
+ **C++ project in a subdirectory:**
234
276
 
235
- Use the `--no-ignore` flag to skip `.shiftignore` rules for a single run:
277
+ ```json
278
+ [
279
+ {
280
+ "language": "cpp",
281
+ "path": "engine/src"
282
+ }
283
+ ]
284
+ ```
236
285
 
237
- ```bash
238
- shift-cli init --no-ignore # Index all files
239
- shift-cli update-drg --no-ignore # Include ignored files in DRG update
286
+ **Mixed JavaScript and Python at root:**
287
+
288
+ ```json
289
+ [
290
+ {
291
+ "language": "javascript",
292
+ "path": ""
293
+ },
294
+ {
295
+ "language": "python",
296
+ "path": ""
297
+ }
298
+ ]
240
299
  ```
241
300
 
242
301
  ## MCP Tools
@@ -255,4 +314,4 @@ Each tool accepts an optional `project_id` parameter. If not provided, it falls
255
314
  |------|----------|-------------|
256
315
  | Global config | `~/.shift/config.json` | API key and server URLs |
257
316
  | Project config | `.shift/config.json` | Project ID, name, and agent info |
258
- | Ignore rules | `.shiftignore` | Files/directories to exclude (auto-created) |
317
+ | Scan targets | `.shift/scan_target.json` | Language and path targets for scanning (auto-created) |
@@ -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.12",
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
  },