@latentforce/shift 1.0.9 → 1.0.10

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,13 +38,15 @@ shift-cli start # Configure API key and project interactively
34
38
  shift-cli init # Index project files
35
39
  ```
36
40
 
41
+ ## MCP Integration
42
+
37
43
  ### Add to Claude Code
38
44
 
39
45
  ```bash
40
46
  claude mcp add-json shift '{"type":"stdio","command":"shift-cli","args":["mcp"],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID"}}'
41
47
  ```
42
48
 
43
- Or using npx:
49
+ Or using npx (no global install needed):
44
50
 
45
51
  ```bash
46
52
  claude mcp add-json shift '{"type":"stdio","command":"npx","args":["@latentforce/shift","mcp"],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID"}}'
@@ -68,52 +74,170 @@ Add to your config file (`%APPDATA%\Claude\claude_desktop_config.json` on Window
68
74
 
69
75
  | Command | Description |
70
76
  |---------|-------------|
71
- | `shift-cli start` | Start daemon and configure project |
77
+ | `shift-cli start` | Start the daemon and configure the project |
72
78
  | `shift-cli init` | Scan and index project files |
73
79
  | `shift-cli update-drg` | Update the dependency relationship graph |
74
80
  | `shift-cli stop` | Stop the daemon |
75
81
  | `shift-cli status` | Show current status |
76
82
  | `shift-cli config` | Manage configuration |
77
83
 
78
- ### CLI Flags
84
+ ### `shift-cli init`
79
85
 
80
- #### `shift-cli init`
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.
87
+
88
+ If the project is already indexed, you will be prompted to re-index. Use `--force` to skip the prompt.
81
89
 
82
90
  | Flag | Description |
83
91
  |------|-------------|
84
- | `--guest` | Use guest authentication (auto-creates project) |
92
+ | `--guest` | Use guest authentication (auto-creates a temporary project) |
85
93
  | `--api-key <key>` | Provide API key directly |
86
94
  | `--project-name <name>` | Create new project or match existing by name |
87
95
  | `--project-id <id>` | Use existing project UUID |
88
96
  | `--template <id>` | Migration template ID for project creation |
89
97
  | `-f, --force` | Force re-indexing even if already indexed |
98
+ | `--no-ignore` | Skip `.shiftignore` rules for this run (send all files) |
90
99
 
91
- #### `shift-cli start`
100
+ ```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
105
+ ```
106
+
107
+ ### `shift-cli start`
108
+
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.
92
110
 
93
111
  | Flag | Description |
94
112
  |------|-------------|
95
- | `--guest` | Use guest authentication (auto-creates project) |
113
+ | `--guest` | Use guest authentication (auto-creates a temporary project) |
96
114
  | `--api-key <key>` | Provide API key directly |
97
115
  | `--project-name <name>` | Create new project or match existing by name |
98
116
  | `--project-id <id>` | Use existing project UUID |
99
117
  | `--template <id>` | Migration template ID for project creation |
100
118
 
101
- #### `shift-cli update-drg`
119
+ ```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"
123
+ ```
124
+
125
+ ### `shift-cli update-drg`
126
+
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.
102
128
 
103
129
  | Flag | Description |
104
130
  |------|-------------|
105
131
  | `-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
+
134
+ **Modes:**
135
+ - **incremental** (default) — sends only git-changed files for faster updates
136
+ - **baseline** — sends all JS/TS files for a full re-analysis
137
+
138
+ ```bash
139
+ shift-cli update-drg # Incremental update (default)
140
+ shift-cli update-drg -m baseline # Full re-analysis
141
+ shift-cli update-drg --no-ignore # Include ignored files
142
+ ```
106
143
 
107
- ### Update DRG
144
+ ### `shift-cli config`
108
145
 
109
- After initializing your project, update the dependency graph to keep code intelligence in sync:
146
+ Manage Shift configuration (URLs, API key).
147
+
148
+ | Action | Description |
149
+ |--------|-------------|
150
+ | `show` | Display current configuration (default) |
151
+ | `set <key> <value>` | Set a configuration value |
152
+ | `clear [key]` | Clear a specific key or all configuration |
153
+
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`.
110
157
 
111
158
  ```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
159
+ shift-cli config # Show config
160
+ 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
+ shift-cli config clear api-key # Clear API key
163
+ shift-cli config clear # Clear all config
114
164
  ```
115
165
 
116
- Scans `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs` files.
166
+ ## `.shiftignore`
167
+
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`.
169
+
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.
171
+
172
+ ### Syntax
173
+
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)
180
+
181
+ ### Default `.shiftignore`
182
+
183
+ The auto-generated file excludes common non-source directories and files:
184
+
185
+ ```gitignore
186
+ # Dependencies
187
+ node_modules/
188
+ vendor/
189
+ bower_components/
190
+
191
+ # Build output
192
+ dist/
193
+ build/
194
+ out/
195
+ .next/
196
+
197
+ # Test & coverage
198
+ coverage/
199
+ .nyc_output/
200
+
201
+ # Environment & secrets
202
+ .env
203
+ .env.*
204
+ *.pem
205
+ *.key
206
+
207
+ # Logs
208
+ *.log
209
+ logs/
210
+
211
+ # OS files
212
+ .DS_Store
213
+ Thumbs.db
214
+
215
+ # IDE
216
+ .vscode/
217
+ .idea/
218
+ *.swp
219
+ *.swo
220
+
221
+ # Python
222
+ __pycache__/
223
+ *.pyc
224
+ venv/
225
+ .venv/
226
+
227
+ # Misc
228
+ *.min.js
229
+ *.min.css
230
+ *.map
231
+ ```
232
+
233
+ ### Bypassing `.shiftignore`
234
+
235
+ Use the `--no-ignore` flag to skip `.shiftignore` rules for a single run:
236
+
237
+ ```bash
238
+ shift-cli init --no-ignore # Index all files
239
+ shift-cli update-drg --no-ignore # Include ignored files in DRG update
240
+ ```
117
241
 
118
242
  ## MCP Tools
119
243
 
@@ -125,6 +249,10 @@ Scans `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs` files.
125
249
 
126
250
  Each tool accepts an optional `project_id` parameter. If not provided, it falls back to the `SHIFT_PROJECT_ID` environment variable.
127
251
 
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
252
+ ## Configuration Files
253
+
254
+ | File | Location | Description |
255
+ |------|----------|-------------|
256
+ | Global config | `~/.shift/config.json` | API key and server URLs |
257
+ | Project config | `.shift/config.json` | Project ID, name, and agent info |
258
+ | Ignore rules | `.shiftignore` | Files/directories to exclude (auto-created) |
@@ -7,6 +7,7 @@ import { fetchProjectStatus, sendInitScan } from '../../utils/api-client.js';
7
7
  import { resolveApiKey, resolveProject } from '../../utils/auth-resolver.js';
8
8
  import { getDaemonStatus, startDaemon } from '../../daemon/daemon-manager.js';
9
9
  import { getProjectTree, extractAllFilePaths, categorizeFiles } from '../../utils/tree-scanner.js';
10
+ import { loadShiftIgnore, scaffoldShiftIgnore } from '../../utils/shiftignore.js';
10
11
  const require = createRequire(import.meta.url);
11
12
  const { version } = require('../../../package.json');
12
13
  const execAsync = promisify(exec);
@@ -120,6 +121,18 @@ export async function initCommand(options = {}) {
120
121
  }
121
122
  // Step 4: Scan project structure (matching extension's Step 6)
122
123
  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
+ }
123
136
  // Get project tree (matching extension's getProjectTree)
124
137
  const treeData = getProjectTree(projectRoot, {
125
138
  depth: 0, // Unlimited depth
@@ -136,6 +149,7 @@ export async function initCommand(options = {}) {
136
149
  'venv',
137
150
  'env',
138
151
  ],
152
+ shiftIgnore,
139
153
  });
140
154
  console.log(`[Init] Files: ${treeData.file_count}`);
141
155
  console.log(`[Init] Directories: ${treeData.dir_count}`);
@@ -1,6 +1,7 @@
1
1
  import { readProjectConfig, setProject } 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';
4
5
  export async function startCommand(options = {}) {
5
6
  const projectRoot = process.cwd();
6
7
  const isAuthInteractive = !options.guest && !options.apiKey;
@@ -37,6 +38,10 @@ export async function startCommand(options = {}) {
37
38
  console.error('❌ Failed to configure project');
38
39
  process.exit(1);
39
40
  }
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
45
  // Step 3: Check if daemon is already running
41
46
  console.log('[Start] Step 3/4: Checking daemon status...');
42
47
  const status = getDaemonStatus(projectRoot);
@@ -4,6 +4,7 @@ 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';
7
8
  import { sendUpdateDrg } from '../../utils/api-client.js';
8
9
  const execAsync = promisify(exec);
9
10
  async function detectGitChanges(projectRoot, extensions) {
@@ -77,6 +78,13 @@ export async function updateDrgCommand(options = {}) {
77
78
  console.log(`[DRG] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
78
79
  // Step 3: Scan project for JS/TS files
79
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
+ }
80
88
  const treeData = getProjectTree(projectRoot, {
81
89
  depth: 0,
82
90
  exclude_patterns: [
@@ -92,6 +100,7 @@ export async function updateDrgCommand(options = {}) {
92
100
  'venv',
93
101
  'env',
94
102
  ],
103
+ shiftIgnore,
95
104
  });
96
105
  const allFiles = extractAllFilePaths(treeData.tree);
97
106
  const jstsFiles = allFiles.filter((filePath) => {
@@ -123,6 +132,10 @@ export async function updateDrgCommand(options = {}) {
123
132
  // Always use git to compute added/modified/deleted (single source of truth)
124
133
  console.log('[DRG] Detecting git changes...');
125
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);
126
139
  console.log(`[DRG] Git changes — added: ${gitChanges.added.length}, modified: ${gitChanges.modified.length}, deleted: ${gitChanges.deleted.length}`);
127
140
  // Which files to send: incremental = only changed; baseline = all (full re-analysis)
128
141
  const changedPaths = new Set([...gitChanges.added, ...gitChanges.modified]);
package/build/index.js CHANGED
@@ -6,16 +6,25 @@ 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\n' +
20
+ ' Ignore rules: .shiftignore (auto-created on first run)')
10
21
  .version(version);
11
22
  // MCP server mode (default when run via MCP host)
12
23
  program
13
24
  .command('mcp', { isDefault: true, hidden: true })
14
25
  .description('Start MCP server on stdio')
15
26
  .action(async () => {
16
- // Check if 'mcp' was explicitly passed as argument
17
27
  const mcpExplicitlyRequested = process.argv.includes('mcp');
18
- // If running interactively (TTY) and mcp wasn't explicitly requested, show help
19
28
  if (process.stdin.isTTY && !mcpExplicitlyRequested) {
20
29
  program.outputHelp();
21
30
  return;
@@ -23,15 +32,15 @@ program
23
32
  const { startMcpServer } = await import('./mcp-server.js');
24
33
  await startMcpServer();
25
34
  });
26
- // CLI commands
27
- program
35
+ // --- start ---
36
+ const startCmd = program
28
37
  .command('start')
29
38
  .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')
39
+ .option('--guest', 'Use guest authentication (auto-creates a temporary project)')
40
+ .option('--api-key <key>', 'Provide your Shift API key directly instead of interactive prompt')
41
+ .option('--project-name <name>', 'Create a new project or match an existing one by name')
42
+ .option('--project-id <id>', 'Link to an existing project by its UUID')
43
+ .option('--template <id>', 'Use a specific migration template when creating the project')
35
44
  .action(async (options) => {
36
45
  const { startCommand } = await import('./cli/commands/start.js');
37
46
  await startCommand({
@@ -42,15 +51,28 @@ program
42
51
  template: options.template,
43
52
  });
44
53
  });
45
- program
54
+ startCmd.addHelpText('after', `
55
+ Details:
56
+ Resolves authentication, configures the project, and launches a
57
+ background daemon that maintains a WebSocket connection to the
58
+ Shift backend. Creates a default .shiftignore if one doesn't exist.
59
+
60
+ Examples:
61
+ shift-cli start Interactive setup
62
+ shift-cli start --guest Quick start without API key
63
+ shift-cli start --api-key <key> --project-name "My App"
64
+ `);
65
+ // --- init ---
66
+ const initCmd = program
46
67
  .command('init')
47
68
  .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')
69
+ .option('-f, --force', 'Force re-indexing even if the project is already indexed')
70
+ .option('--guest', 'Use guest authentication (auto-creates a temporary project)')
71
+ .option('--api-key <key>', 'Provide your Shift API key directly instead of interactive prompt')
72
+ .option('--project-name <name>', 'Create a new project or match an existing one by name')
73
+ .option('--project-id <id>', 'Link to an existing project by its UUID')
74
+ .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)')
54
76
  .action(async (options) => {
55
77
  const { initCommand } = await import('./cli/commands/init.js');
56
78
  await initCommand({
@@ -60,35 +82,108 @@ program
60
82
  projectName: options.projectName,
61
83
  projectId: options.projectId,
62
84
  template: options.template,
85
+ noIgnore: options.noIgnore,
63
86
  });
64
87
  });
65
- program
88
+ initCmd.addHelpText('after', `
89
+ Details:
90
+ Performs a full project scan — collects the file tree, categorizes files
91
+ (source, config, assets), gathers git info, and sends everything to the
92
+ Shift backend for indexing. Automatically starts the daemon if not running.
93
+ Creates a default .shiftignore if one doesn't exist.
94
+
95
+ If the project is already indexed, you will be prompted to re-index.
96
+ Use --force to skip the prompt.
97
+
98
+ Examples:
99
+ shift-cli init Interactive initialization
100
+ shift-cli init --force Force re-index without prompt
101
+ shift-cli init --no-ignore Index all files, ignore .shiftignore
102
+ shift-cli init --guest Quick init with guest auth
103
+ `);
104
+ // --- stop ---
105
+ const stopCmd = program
66
106
  .command('stop')
67
- .description('Stop the Shift daemon')
107
+ .description('Stop the Shift daemon for this project')
68
108
  .action(async () => {
69
109
  const { stopCommand } = await import('./cli/commands/stop.js');
70
110
  await stopCommand();
71
111
  });
72
- program
112
+ stopCmd.addHelpText('after', `
113
+ Details:
114
+ Terminates the background daemon process. The daemon can be
115
+ restarted later with "shift-cli start".
116
+ `);
117
+ // --- status ---
118
+ const statusCmd = program
73
119
  .command('status')
74
120
  .description('Show the current Shift status')
75
121
  .action(async () => {
76
122
  const { statusCommand } = await import('./cli/commands/status.js');
77
123
  await statusCommand();
78
124
  });
79
- program
125
+ statusCmd.addHelpText('after', `
126
+ Details:
127
+ Displays a comprehensive overview of:
128
+ - API key configuration (guest or authenticated)
129
+ - Project details (name, ID)
130
+ - Registered agents
131
+ - Backend indexing status and file count
132
+ - Daemon process status (PID, WebSocket connection, uptime)
133
+ `);
134
+ // --- update-drg ---
135
+ const updateDrgCmd = program
80
136
  .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')
137
+ .description('Update the dependency relationship graph (DRG)')
138
+ .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)')
83
140
  .action(async (options) => {
84
141
  const { updateDrgCommand } = await import('./cli/commands/update-drg.js');
85
- await updateDrgCommand({ mode: options.mode });
142
+ await updateDrgCommand({ mode: options.mode, noIgnore: options.noIgnore });
86
143
  });
87
- program
144
+ updateDrgCmd.addHelpText('after', `
145
+ Details:
146
+ Scans JavaScript/TypeScript files (.js, .jsx, .ts, .tsx, .mjs, .cjs),
147
+ detects git changes, and sends file contents to the backend for
148
+ dependency analysis. Respects .shiftignore rules.
149
+
150
+ Modes:
151
+ incremental Send only git-changed files (default, faster)
152
+ baseline Send all JS/TS files (full re-analysis)
153
+
154
+ Examples:
155
+ shift-cli update-drg Incremental update
156
+ shift-cli update-drg -m baseline Full re-analysis
157
+ shift-cli update-drg --no-ignore Include ignored files
158
+ `);
159
+ // --- config ---
160
+ const configCmd = program
88
161
  .command('config [action] [key] [value]')
89
162
  .description('Manage Shift configuration (URLs, API key)')
90
163
  .action(async (action, key, value) => {
91
164
  const { configCommand } = await import('./cli/commands/config.js');
92
165
  await configCommand(action, key, value);
93
166
  });
167
+ configCmd.addHelpText('after', `
168
+ Actions:
169
+ show Display current configuration (default)
170
+ set <key> <val> Set a configuration value
171
+ clear [key] Clear a specific key or all configuration
172
+
173
+ Configurable keys:
174
+ api-key Your Shift API key
175
+ api-url Backend API URL (default: http://localhost:9000)
176
+ orch-url Orchestrator URL (default: http://localhost:9999)
177
+ ws-url WebSocket URL (default: ws://localhost:9999)
178
+
179
+ URLs can also be set via environment variables:
180
+ SHIFT_API_URL, SHIFT_ORCH_URL, SHIFT_WS_URL
181
+
182
+ Examples:
183
+ shift-cli config Show config
184
+ shift-cli config set api-key sk-abc123 Set API key
185
+ shift-cli config set api-url https://api.shift.ai Set API URL
186
+ shift-cli config clear api-key Clear API key
187
+ shift-cli config clear Clear all config
188
+ `);
94
189
  program.parse();
@@ -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
+ }
@@ -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, } = options;
24
+ const { depth = 0, exclude_patterns = DEFAULT_EXCLUDE_PATTERNS, shiftIgnore = null, } = options;
25
25
  let file_count = 0;
26
26
  let dir_count = 0;
27
27
  let total_size = 0;
@@ -34,12 +34,21 @@ 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
  }
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
+ }
43
52
  if (entry.isDirectory()) {
44
53
  dir_count++;
45
54
  const children = scanDirectory(itemPath, currentDepth + 1, itemRelativePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latentforce/shift",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Shift CLI - AI-powered code intelligence with MCP support",
5
5
  "type": "module",
6
6
  "main": "./build/index.js",
@@ -38,6 +38,7 @@
38
38
  "dependencies": {
39
39
  "@modelcontextprotocol/sdk": "^1.25.3",
40
40
  "commander": "^12.0.0",
41
+ "ignore": "^7.0.5",
41
42
  "ws": "^8.14.0",
42
43
  "zod": "^3.25.76"
43
44
  },