@latentforce/shift 1.0.9 → 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
@@ -2,12 +2,16 @@
2
2
 
3
3
  AI-powered code intelligence CLI and MCP server for Claude.
4
4
 
5
+ Shift indexes your codebase, builds a dependency relationship graph (DRG), and provides AI-powered insights via MCP tools — enabling Claude to understand your project's structure, dependencies, and blast radius of changes.
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
8
10
  npm install -g @latentforce/shift
9
11
  ```
10
12
 
13
+ Requires Node.js >= 18.
14
+
11
15
  ## Quick Start
12
16
 
13
17
  ### Guest mode (zero-config)
@@ -34,19 +38,27 @@ shift-cli start # Configure API key and project interactively
34
38
  shift-cli init # Index project files
35
39
  ```
36
40
 
37
- ### Add to Claude Code
41
+ ## MCP Integration
42
+
43
+ After initializing your project, use `shift-cli add` to configure Shift's MCP server in your AI coding tool:
38
44
 
39
45
  ```bash
40
- claude mcp add-json shift '{"type":"stdio","command":"shift-cli","args":["mcp"],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID"}}'
46
+ shift-cli add <tool>
41
47
  ```
42
48
 
43
- Or using npx:
49
+ ### Supported tools
44
50
 
45
- ```bash
46
- claude mcp add-json shift '{"type":"stdio","command":"npx","args":["@latentforce/shift","mcp"],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID"}}'
47
- ```
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` |
58
+
59
+ For CLI-based tools, if the CLI is not installed, the command will print manual setup instructions.
48
60
 
49
- ### Add to Claude Desktop
61
+ ### Claude Desktop (manual)
50
62
 
51
63
  Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Windows, `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
52
64
 
@@ -68,52 +80,179 @@ Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Window
68
80
 
69
81
  | Command | Description |
70
82
  |---------|-------------|
71
- | `shift-cli start` | Start daemon and configure project |
83
+ | `shift-cli start` | Start the daemon and configure the project |
72
84
  | `shift-cli init` | Scan and index project files |
73
85
  | `shift-cli update-drg` | Update the dependency relationship graph |
86
+ | `shift-cli add <tool>` | Add Shift MCP server to an AI coding tool |
74
87
  | `shift-cli stop` | Stop the daemon |
75
88
  | `shift-cli status` | Show current status |
76
89
  | `shift-cli config` | Manage configuration |
77
90
 
78
- ### CLI Flags
91
+ ### `shift-cli init`
92
+
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.
79
94
 
80
- #### `shift-cli init`
95
+ If the project is already indexed, you will be prompted to re-index. Use `--force` to skip the prompt.
81
96
 
82
97
  | Flag | Description |
83
98
  |------|-------------|
84
- | `--guest` | Use guest authentication (auto-creates project) |
99
+ | `--guest` | Use guest authentication (auto-creates a temporary project) |
85
100
  | `--api-key <key>` | Provide API key directly |
86
101
  | `--project-name <name>` | Create new project or match existing by name |
87
102
  | `--project-id <id>` | Use existing project UUID |
88
103
  | `--template <id>` | Migration template ID for project creation |
89
104
  | `-f, --force` | Force re-indexing even if already indexed |
90
105
 
91
- #### `shift-cli start`
106
+ ```bash
107
+ shift-cli init # Interactive initialization
108
+ shift-cli init --force # Force re-index without prompt
109
+ shift-cli init --guest # Quick init with guest auth
110
+ ```
111
+
112
+ ### `shift-cli start`
113
+
114
+ Resolves authentication, configures the project, and launches a background daemon that maintains a WebSocket connection to the Shift backend.
92
115
 
93
116
  | Flag | Description |
94
117
  |------|-------------|
95
- | `--guest` | Use guest authentication (auto-creates project) |
118
+ | `--guest` | Use guest authentication (auto-creates a temporary project) |
96
119
  | `--api-key <key>` | Provide API key directly |
97
120
  | `--project-name <name>` | Create new project or match existing by name |
98
121
  | `--project-id <id>` | Use existing project UUID |
99
122
  | `--template <id>` | Migration template ID for project creation |
100
123
 
101
- #### `shift-cli update-drg`
124
+ ```bash
125
+ shift-cli start # Interactive setup
126
+ shift-cli start --guest # Quick start without API key
127
+ shift-cli start --api-key <key> --project-name "My App"
128
+ ```
129
+
130
+ ### `shift-cli update-drg`
131
+
132
+ Scans JavaScript/TypeScript files (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`), detects git changes, and sends file contents to the backend for dependency analysis.
102
133
 
103
134
  | Flag | Description |
104
135
  |------|-------------|
105
136
  | `-m, --mode <mode>` | `baseline` (all files) or `incremental` (git-changed only, default) |
106
137
 
107
- ### Update DRG
138
+ **Modes:**
139
+ - **incremental** (default) — sends only git-changed files for faster updates
140
+ - **baseline** — sends all JS/TS files for a full re-analysis
141
+
142
+ ```bash
143
+ shift-cli update-drg # Incremental update (default)
144
+ shift-cli update-drg -m baseline # Full re-analysis
145
+ ```
146
+
147
+ ### `shift-cli add <tool>`
108
148
 
109
- After initializing your project, update the dependency graph to keep code intelligence in sync:
149
+ Configures Shift's MCP server in the specified AI coding tool. Reads `project_id` from `.shift/config.json` (run `shift-cli init` first).
110
150
 
111
151
  ```bash
112
- shift-cli update-drg # Incremental (default) only git-changed files
113
- shift-cli update-drg --mode baseline # Full re-scan of all JS/TS files
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
114
157
  ```
115
158
 
116
- Scans `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs` files.
159
+ ### `shift-cli config`
160
+
161
+ Manage Shift configuration (URLs, API key).
162
+
163
+ | Action | Description |
164
+ |--------|-------------|
165
+ | `show` | Display current configuration (default) |
166
+ | `set <key> <value>` | Set a configuration value |
167
+ | `clear [key]` | Clear a specific key or all configuration |
168
+
169
+ **Configurable keys:** `api-key`, `api-url`, `orch-url`, `ws-url`
170
+
171
+ URLs can also be set via environment variables: `SHIFT_API_URL`, `SHIFT_ORCH_URL`, `SHIFT_WS_URL`.
172
+
173
+ ```bash
174
+ shift-cli config # Show config
175
+ shift-cli config set api-key sk-abc123 # Set API key
176
+ shift-cli config set api-url https://api.shift.ai # Set API URL
177
+ shift-cli config clear api-key # Clear API key
178
+ shift-cli config clear # Clear all config
179
+ ```
180
+
181
+ ## `.shiftignore`
182
+
183
+ 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`.
184
+
185
+ 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.
186
+
187
+ ### Syntax
188
+
189
+ - Blank lines are ignored
190
+ - Lines starting with `#` are comments
191
+ - Standard glob patterns (`*`, `**`, `?`)
192
+ - Trailing `/` matches directories only
193
+ - Leading `/` anchors to the project root
194
+ - `!` negates a pattern (re-includes a previously ignored path)
195
+
196
+ ### Default `.shiftignore`
197
+
198
+ The auto-generated file excludes common non-source directories and files:
199
+
200
+ ```gitignore
201
+ # Dependencies
202
+ node_modules/
203
+ vendor/
204
+ bower_components/
205
+
206
+ # Build output
207
+ dist/
208
+ build/
209
+ out/
210
+ .next/
211
+
212
+ # Test & coverage
213
+ coverage/
214
+ .nyc_output/
215
+
216
+ # Environment & secrets
217
+ .env
218
+ .env.*
219
+ *.pem
220
+ *.key
221
+
222
+ # Logs
223
+ *.log
224
+ logs/
225
+
226
+ # OS files
227
+ .DS_Store
228
+ Thumbs.db
229
+
230
+ # IDE
231
+ .vscode/
232
+ .idea/
233
+ *.swp
234
+ *.swo
235
+
236
+ # Python
237
+ __pycache__/
238
+ *.pyc
239
+ venv/
240
+ .venv/
241
+
242
+ # Misc
243
+ *.min.js
244
+ *.min.css
245
+ *.map
246
+ ```
247
+
248
+ ### Bypassing `.shiftignore`
249
+
250
+ Use the `--no-ignore` flag to skip `.shiftignore` rules for a single run:
251
+
252
+ ```bash
253
+ shift-cli init --no-ignore # Index all files
254
+ shift-cli update-drg --no-ignore # Include ignored files in DRG update
255
+ ```
117
256
 
118
257
  ## MCP Tools
119
258
 
@@ -125,6 +264,10 @@ Scans `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs` files.
125
264
 
126
265
  Each tool accepts an optional `project_id` parameter. If not provided, it falls back to the `SHIFT_PROJECT_ID` environment variable.
127
266
 
128
- MCP tools return formatted markdown and include:
129
- - **Path normalization** — backslashes, leading `./` and `/` are handled automatically
130
- - **Actionable error messages** missing graph prompts you to run `update-drg`, missing files explain possible causes
267
+ ## Configuration Files
268
+
269
+ | File | Location | Description |
270
+ |------|----------|-------------|
271
+ | Global config | `~/.shift/config.json` | API key and server URLs |
272
+ | Project config | `.shift/config.json` | Project ID, name, and agent info |
273
+ | Ignore rules | `.shiftignore` | Files/directories to exclude (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,11 +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';
11
+ import { getProjectTree, extractAllFilePaths, categorizeFiles, countProjectLOC } from '../../utils/tree-scanner.js';
10
12
  const require = createRequire(import.meta.url);
11
13
  const { version } = require('../../../package.json');
12
14
  const execAsync = promisify(exec);
@@ -63,6 +65,8 @@ export async function initCommand(options = {}) {
63
65
  console.error('\n❌ Failed to configure project.');
64
66
  process.exit(1);
65
67
  }
68
+ // Ensure .shift/scan_target.json exists (template for user customization)
69
+ ensureScanTargetFile(projectRoot);
66
70
  // Check if project is already indexed (skip check if --force flag is used)
67
71
  if (!options.force) {
68
72
  try {
@@ -160,11 +164,49 @@ export async function initCommand(options = {}) {
160
164
  // Extract file paths and categorize (matching extension)
161
165
  const allFiles = extractAllFilePaths(treeData.tree);
162
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}`);
163
170
  console.log(`[Init] Source files: ${categorized.source_files.length}`);
164
171
  console.log(`[Init] Config files: ${categorized.config_files.length}`);
165
172
  console.log(`[Init] Asset files: ${categorized.asset_files.length}\n`);
166
173
  // Step 5: Send scan to backend (matching extension's Step 9)
167
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
+ }
168
210
  const payload = {
169
211
  project_id: project.projectId,
170
212
  project_tree: treeData,
@@ -178,6 +220,8 @@ export async function initCommand(options = {}) {
178
220
  scan_timestamp: new Date().toISOString(),
179
221
  project_name: project.projectName,
180
222
  },
223
+ scan_targets: scanTargets,
224
+ total_loc: totalLOC,
181
225
  };
182
226
  try {
183
227
  const response = await sendInitScan(apiKey, project.projectId, payload);
@@ -1,4 +1,4 @@
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
4
  export async function startCommand(options = {}) {
@@ -37,6 +37,8 @@ export async function startCommand(options = {}) {
37
37
  console.error('❌ Failed to configure project');
38
38
  process.exit(1);
39
39
  }
40
+ // Ensure .shift/scan_target.json exists (template for user customization)
41
+ ensureScanTargetFile(projectRoot);
40
42
  // Step 3: Check if daemon is already running
41
43
  console.log('[Start] Step 3/4: Checking daemon status...');
42
44
  const status = getDaemonStatus(projectRoot);
@@ -9,7 +9,7 @@ const execAsync = promisify(exec);
9
9
  async function detectGitChanges(projectRoot, extensions) {
10
10
  const changes = { added: [], modified: [], deleted: [] };
11
11
  try {
12
- const { stdout } = await execAsync('git status --porcelain', { cwd: projectRoot });
12
+ const { stdout } = await execAsync('git status --porcelain -u', { cwd: projectRoot });
13
13
  for (const line of stdout.split('\n')) {
14
14
  if (!line.trim())
15
15
  continue;
@@ -51,7 +51,24 @@ async function detectGitChanges(projectRoot, extensions) {
51
51
  }
52
52
  return changes;
53
53
  }
54
- 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
+ };
55
72
  export async function updateDrgCommand(options = {}) {
56
73
  const projectRoot = process.cwd();
57
74
  const mode = options.mode || 'incremental';
@@ -75,8 +92,8 @@ export async function updateDrgCommand(options = {}) {
75
92
  process.exit(1);
76
93
  }
77
94
  console.log(`[DRG] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
78
- // Step 3: Scan project for JS/TS files
79
- console.log('[DRG] Step 3/4: Scanning project for JS/TS files...');
95
+ // Step 3: Scan project for all supported language files
96
+ console.log('[DRG] Step 3/4: Scanning project for supported languages...');
80
97
  const treeData = getProjectTree(projectRoot, {
81
98
  depth: 0,
82
99
  exclude_patterns: [
@@ -94,82 +111,84 @@ export async function updateDrgCommand(options = {}) {
94
111
  ],
95
112
  });
96
113
  const allFiles = extractAllFilePaths(treeData.tree);
97
- const jstsFiles = allFiles.filter((filePath) => {
98
- const ext = path.extname(filePath).toLowerCase();
99
- return JS_TS_EXTENSIONS.has(ext);
100
- });
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);
101
121
  console.log(`[DRG] Total files scanned: ${allFiles.length}`);
102
- console.log(`[DRG] JS/TS files found: ${jstsFiles.length}\n`);
103
- if (jstsFiles.length === 0) {
104
- 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');
105
128
  return;
106
129
  }
107
- // Read file contents
108
- const files = [];
109
- let readErrors = 0;
110
- for (const filePath of jstsFiles) {
111
- const absolutePath = path.join(projectRoot, filePath);
112
- try {
113
- const content = fs.readFileSync(absolutePath, 'utf-8');
114
- 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
+ }
115
147
  }
116
- catch {
117
- readErrors++;
148
+ if (readErrors > 0) {
149
+ console.log(`[DRG] [${label}] ⚠️ Could not read ${readErrors} file(s)\n`);
118
150
  }
119
- }
120
- if (readErrors > 0) {
121
- console.log(`[DRG] ⚠️ Could not read ${readErrors} file(s)\n`);
122
- }
123
- // Always use git to compute added/modified/deleted (single source of truth)
124
- console.log('[DRG] Detecting git changes...');
125
- const gitChanges = await detectGitChanges(projectRoot, JS_TS_EXTENSIONS);
126
- console.log(`[DRG] Git changes — added: ${gitChanges.added.length}, modified: ${gitChanges.modified.length}, deleted: ${gitChanges.deleted.length}`);
127
- // Which files to send: incremental = only changed; baseline = all (full re-analysis)
128
- const changedPaths = new Set([...gitChanges.added, ...gitChanges.modified]);
129
- const filesToSend = mode === 'baseline'
130
- ? files
131
- : files.filter((f) => changedPaths.has(f.file_path));
132
- if (mode === 'baseline') {
133
- console.log(`[DRG] Baseline: sending all ${filesToSend.length} JS/TS files.\n`);
134
- }
135
- else {
136
- console.log(`[DRG] Incremental: sending ${filesToSend.length} changed file(s).\n`);
137
- }
138
- if (filesToSend.length === 0 && gitChanges.deleted.length === 0) {
139
- console.log('\n✓ No JS/TS changes detected. DRG is up to date.\n');
140
- return;
141
- }
142
- // Step 4: Send to backend
143
- console.log(`[DRG] Step 4/4: Sending ${filesToSend.length} files to backend (mode: ${mode})...`);
144
- const payload = {
145
- project_id: projectConfig.project_id,
146
- language: 'js_ts',
147
- mode: mode,
148
- changes: gitChanges,
149
- files: filesToSend,
150
- };
151
- try {
152
- const response = await sendUpdateDrg(apiKey, payload);
153
- console.log('\n╔═══════════════════════════════════════════════╗');
154
- console.log('║ ✓ DRG Update Complete ║');
155
- console.log('╚═══════════════════════════════════════════════╝\n');
156
- console.log(` Success: ${response.success}`);
157
- console.log(` Message: ${response.message}`);
158
- if (response.stats) {
159
- console.log(` Files sent: ${response.stats.total_files_provided}`);
160
- console.log(` Unique: ${response.stats.unique_paths}`);
161
- console.log(` Added: ${response.stats.added}`);
162
- console.log(` Modified: ${response.stats.modified}`);
163
- console.log(` Deleted: ${response.stats.deleted}`);
164
- const affected = response.stats.affected_file_paths;
165
- if (affected?.length) {
166
- 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}`);
167
175
  }
168
176
  }
169
- 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('');
170
182
  }
171
- catch (error) {
172
- 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}`);
173
189
  process.exit(1);
174
190
  }
191
+ if (lastError) {
192
+ console.log(`⚠️ Some languages failed (see above).`);
193
+ }
175
194
  }
package/build/index.js CHANGED
@@ -6,16 +6,24 @@ const { version } = require('../package.json');
6
6
  const program = new Command();
7
7
  program
8
8
  .name('shift-cli')
9
- .description('Shift CLI - AI-powered code intelligence')
9
+ .description('Shift CLI - AI-powered code intelligence and dependency analysis.\n\n' +
10
+ 'Shift indexes your codebase, builds a dependency relationship graph (DRG),\n' +
11
+ 'and provides AI-powered insights via MCP tools.\n\n' +
12
+ 'Quick start:\n' +
13
+ ' shift-cli start Start the daemon and configure project\n' +
14
+ ' shift-cli init Scan and index the project\n' +
15
+ ' shift-cli update-drg Build/update the dependency graph\n' +
16
+ ' shift-cli status Check current status\n\n' +
17
+ 'Configuration:\n' +
18
+ ' Global config: ~/.shift/config.json\n' +
19
+ ' Project config: .shift/config.json')
10
20
  .version(version);
11
21
  // MCP server mode (default when run via MCP host)
12
22
  program
13
23
  .command('mcp', { isDefault: true, hidden: true })
14
24
  .description('Start MCP server on stdio')
15
25
  .action(async () => {
16
- // Check if 'mcp' was explicitly passed as argument
17
26
  const mcpExplicitlyRequested = process.argv.includes('mcp');
18
- // If running interactively (TTY) and mcp wasn't explicitly requested, show help
19
27
  if (process.stdin.isTTY && !mcpExplicitlyRequested) {
20
28
  program.outputHelp();
21
29
  return;
@@ -23,15 +31,15 @@ program
23
31
  const { startMcpServer } = await import('./mcp-server.js');
24
32
  await startMcpServer();
25
33
  });
26
- // CLI commands
27
- program
34
+ // --- start ---
35
+ const startCmd = program
28
36
  .command('start')
29
37
  .description('Start the Shift daemon for this project')
30
- .option('--guest', 'Use guest authentication (auto-creates project)')
31
- .option('--api-key <key>', 'Provide API key directly')
32
- .option('--project-name <name>', 'Create new project or match existing by name')
33
- .option('--project-id <id>', 'Use existing project UUID')
34
- .option('--template <id>', 'Migration template ID for project creation')
38
+ .option('--guest', 'Use guest authentication (auto-creates a temporary project)')
39
+ .option('--api-key <key>', 'Provide your Shift API key directly instead of interactive prompt')
40
+ .option('--project-name <name>', 'Create a new project or match an existing one by name')
41
+ .option('--project-id <id>', 'Link to an existing project by its UUID')
42
+ .option('--template <id>', 'Use a specific migration template when creating the project')
35
43
  .action(async (options) => {
36
44
  const { startCommand } = await import('./cli/commands/start.js');
37
45
  await startCommand({
@@ -42,15 +50,27 @@ program
42
50
  template: options.template,
43
51
  });
44
52
  });
45
- program
53
+ startCmd.addHelpText('after', `
54
+ Details:
55
+ Resolves authentication, configures the project, and launches a
56
+ background daemon that maintains a WebSocket connection to the
57
+ Shift backend.
58
+
59
+ Examples:
60
+ shift-cli start Interactive setup
61
+ shift-cli start --guest Quick start without API key
62
+ shift-cli start --api-key <key> --project-name "My App"
63
+ `);
64
+ // --- init ---
65
+ const initCmd = program
46
66
  .command('init')
47
67
  .description('Initialize and scan the project for file indexing')
48
- .option('-f, --force', 'Force re-indexing even if project is already indexed')
49
- .option('--guest', 'Use guest authentication (auto-creates project)')
50
- .option('--api-key <key>', 'Provide API key directly')
51
- .option('--project-name <name>', 'Create new project or match existing by name')
52
- .option('--project-id <id>', 'Use existing project UUID')
53
- .option('--template <id>', 'Migration template ID for project creation')
68
+ .option('-f, --force', 'Force re-indexing even if the project is already indexed')
69
+ .option('--guest', 'Use guest authentication (auto-creates a temporary project)')
70
+ .option('--api-key <key>', 'Provide your Shift API key directly instead of interactive prompt')
71
+ .option('--project-name <name>', 'Create a new project or match an existing one by name')
72
+ .option('--project-id <id>', 'Link to an existing project by its UUID')
73
+ .option('--template <id>', 'Use a specific migration template when creating the project')
54
74
  .action(async (options) => {
55
75
  const { initCommand } = await import('./cli/commands/init.js');
56
76
  await initCommand({
@@ -62,33 +82,122 @@ program
62
82
  template: options.template,
63
83
  });
64
84
  });
65
- program
85
+ initCmd.addHelpText('after', `
86
+ Details:
87
+ Performs a full project scan — collects the file tree, categorizes files
88
+ (source, config, assets), gathers git info, and sends everything to the
89
+ Shift backend for indexing. Automatically starts the daemon if not running.
90
+
91
+ If the project is already indexed, you will be prompted to re-index.
92
+ Use --force to skip the prompt.
93
+
94
+ Examples:
95
+ shift-cli init Interactive initialization
96
+ shift-cli init --force Force re-index without prompt
97
+ shift-cli init --guest Quick init with guest auth
98
+ `);
99
+ // --- stop ---
100
+ const stopCmd = program
66
101
  .command('stop')
67
- .description('Stop the Shift daemon')
102
+ .description('Stop the Shift daemon for this project')
68
103
  .action(async () => {
69
104
  const { stopCommand } = await import('./cli/commands/stop.js');
70
105
  await stopCommand();
71
106
  });
72
- program
107
+ stopCmd.addHelpText('after', `
108
+ Details:
109
+ Terminates the background daemon process. The daemon can be
110
+ restarted later with "shift-cli start".
111
+ `);
112
+ // --- status ---
113
+ const statusCmd = program
73
114
  .command('status')
74
115
  .description('Show the current Shift status')
75
116
  .action(async () => {
76
117
  const { statusCommand } = await import('./cli/commands/status.js');
77
118
  await statusCommand();
78
119
  });
79
- program
120
+ statusCmd.addHelpText('after', `
121
+ Details:
122
+ Displays a comprehensive overview of:
123
+ - API key configuration (guest or authenticated)
124
+ - Project details (name, ID)
125
+ - Registered agents
126
+ - Backend indexing status and file count
127
+ - Daemon process status (PID, WebSocket connection, uptime)
128
+ `);
129
+ // --- update-drg ---
130
+ const updateDrgCmd = program
80
131
  .command('update-drg')
81
- .description('Update the dependency relationship graph (DRG) for this project')
82
- .option('-m, --mode <mode>', 'Update mode: baseline (all files) or incremental (git-changed only)', 'incremental')
132
+ .description('Update the dependency relationship graph (DRG)')
133
+ .option('-m, --mode <mode>', 'Update mode: "baseline" (all files) or "incremental" (git-changed only)', 'incremental')
83
134
  .action(async (options) => {
84
135
  const { updateDrgCommand } = await import('./cli/commands/update-drg.js');
85
136
  await updateDrgCommand({ mode: options.mode });
86
137
  });
87
- program
138
+ updateDrgCmd.addHelpText('after', `
139
+ Details:
140
+ Scans JavaScript/TypeScript files (.js, .jsx, .ts, .tsx, .mjs, .cjs),
141
+ detects git changes, and sends file contents to the backend for
142
+ dependency analysis.
143
+
144
+ Modes:
145
+ incremental Send only git-changed files (default, faster)
146
+ baseline Send all JS/TS files (full re-analysis)
147
+
148
+ Examples:
149
+ shift-cli update-drg Incremental update
150
+ shift-cli update-drg -m baseline Full re-analysis
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
172
+ `);
173
+ // --- config ---
174
+ const configCmd = program
88
175
  .command('config [action] [key] [value]')
89
176
  .description('Manage Shift configuration (URLs, API key)')
90
177
  .action(async (action, key, value) => {
91
178
  const { configCommand } = await import('./cli/commands/config.js');
92
179
  await configCommand(action, key, value);
93
180
  });
181
+ configCmd.addHelpText('after', `
182
+ Actions:
183
+ show Display current configuration (default)
184
+ set <key> <val> Set a configuration value
185
+ clear [key] Clear a specific key or all configuration
186
+
187
+ Configurable keys:
188
+ api-key Your Shift API key
189
+ api-url Backend API URL (default: http://localhost:9000)
190
+ orch-url Orchestrator URL (default: http://localhost:9999)
191
+ ws-url WebSocket URL (default: ws://localhost:9999)
192
+
193
+ URLs can also be set via environment variables:
194
+ SHIFT_API_URL, SHIFT_ORCH_URL, SHIFT_WS_URL
195
+
196
+ Examples:
197
+ shift-cli config Show config
198
+ shift-cli config set api-key sk-abc123 Set API key
199
+ shift-cli config set api-url https://api.shift.ai Set API URL
200
+ shift-cli config clear api-key Clear API key
201
+ shift-cli config clear Clear all config
202
+ `);
94
203
  program.parse();
@@ -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);
@@ -0,0 +1,108 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import ignore from 'ignore';
4
+ const SHIFTIGNORE_FILE = '.shiftignore';
5
+ /**
6
+ * Load and parse a .shiftignore file from the project root.
7
+ * Returns an `ignore` instance that can test paths, or null if no .shiftignore exists.
8
+ *
9
+ * .shiftignore uses the same syntax as .gitignore:
10
+ * - Blank lines are ignored
11
+ * - Lines starting with # are comments
12
+ * - Standard glob patterns (*, **, ?)
13
+ * - Trailing / matches directories only
14
+ * - Leading / anchors to root
15
+ * - ! negates a pattern
16
+ */
17
+ const DEFAULT_SHIFTIGNORE = `# .shiftignore — files and directories to exclude from Shift indexing
18
+ # Uses the same syntax as .gitignore
19
+
20
+ # Dependencies
21
+ node_modules/
22
+ vendor/
23
+ bower_components/
24
+
25
+ # Build output
26
+ dist/
27
+ build/
28
+ out/
29
+ .next/
30
+
31
+ # Test & coverage
32
+ coverage/
33
+ .nyc_output/
34
+
35
+ # Environment & secrets
36
+ .env
37
+ .env.*
38
+ *.pem
39
+ *.key
40
+
41
+ # Logs
42
+ *.log
43
+ logs/
44
+
45
+ # OS files
46
+ .DS_Store
47
+ Thumbs.db
48
+
49
+ # IDE
50
+ .vscode/
51
+ .idea/
52
+ *.swp
53
+ *.swo
54
+
55
+ # Python
56
+ __pycache__/
57
+ *.pyc
58
+ venv/
59
+ .venv/
60
+
61
+ # Misc
62
+ *.min.js
63
+ *.min.css
64
+ *.map
65
+ `;
66
+ /**
67
+ * Create a default .shiftignore file in the project root if one doesn't exist.
68
+ * Returns true if a new file was created, false if it already exists.
69
+ */
70
+ export function scaffoldShiftIgnore(projectRoot) {
71
+ const ignorePath = path.join(projectRoot, SHIFTIGNORE_FILE);
72
+ if (fs.existsSync(ignorePath)) {
73
+ return false;
74
+ }
75
+ try {
76
+ fs.writeFileSync(ignorePath, DEFAULT_SHIFTIGNORE, 'utf-8');
77
+ return true;
78
+ }
79
+ catch (err) {
80
+ console.warn(`[ShiftIgnore] Warning: Could not create ${SHIFTIGNORE_FILE}: ${err.message}`);
81
+ return false;
82
+ }
83
+ }
84
+ export function loadShiftIgnore(projectRoot) {
85
+ const ignorePath = path.join(projectRoot, SHIFTIGNORE_FILE);
86
+ if (!fs.existsSync(ignorePath)) {
87
+ return null;
88
+ }
89
+ try {
90
+ const content = fs.readFileSync(ignorePath, 'utf-8');
91
+ const ig = ignore();
92
+ ig.add(content);
93
+ return ig;
94
+ }
95
+ catch (err) {
96
+ console.warn(`[ShiftIgnore] Warning: Could not read ${SHIFTIGNORE_FILE}: ${err.message}`);
97
+ return null;
98
+ }
99
+ }
100
+ /**
101
+ * Filter an array of file paths through the shiftIgnore rules.
102
+ * Returns only paths that are NOT ignored.
103
+ */
104
+ export function filterPaths(ig, paths) {
105
+ if (!ig)
106
+ return paths;
107
+ return paths.filter(p => !ig.ignores(p.replace(/\\/g, '/')));
108
+ }
@@ -34,7 +34,7 @@ export function getProjectTree(workspaceRoot, options = {}) {
34
34
  try {
35
35
  const entries = fs.readdirSync(dirPath, { withFileTypes: true });
36
36
  for (const entry of entries) {
37
- // Check if should be excluded
37
+ // Check if should be excluded by built-in patterns
38
38
  if (exclude_patterns.some(pattern => entry.name.includes(pattern))) {
39
39
  continue;
40
40
  }
@@ -114,6 +114,23 @@ export function extractAllFilePaths(tree, basePath = '') {
114
114
  * Categorize files by type
115
115
  * Matching extension's categorizeFiles function
116
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
+ }
117
134
  export function categorizeFiles(tree, basePath = '') {
118
135
  const categories = {
119
136
  source_files: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latentforce/shift",
3
- "version": "1.0.9",
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",