@latentforce/shift 1.0.1 → 1.0.3
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 +165 -58
- package/build/cli/commands/config.js +136 -0
- package/build/cli/commands/init.js +156 -0
- package/build/cli/commands/start.js +100 -0
- package/build/cli/commands/status.js +51 -0
- package/build/cli/commands/stop.js +18 -0
- package/build/daemon/daemon-manager.js +136 -0
- package/build/daemon/daemon.js +119 -0
- package/build/daemon/tools-executor.js +383 -0
- package/build/daemon/websocket-client.js +334 -0
- package/build/index.js +46 -126
- package/build/mcp-server.js +124 -0
- package/build/utils/api-client.js +66 -0
- package/build/utils/config.js +242 -0
- package/build/utils/prompts.js +69 -0
- package/build/utils/tree-scanner.js +148 -0
- package/package.json +5 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { API_BASE_URL, API_BASE_URL_ORCH } from './config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Request a guest API key
|
|
4
|
+
*/
|
|
5
|
+
export async function requestGuestKey() {
|
|
6
|
+
const response = await fetch(`${API_BASE_URL}/api/guest-key`, {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
headers: {
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
const text = await response.text();
|
|
14
|
+
throw new Error(text || `HTTP ${response.status}`);
|
|
15
|
+
}
|
|
16
|
+
return await response.json();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Fetch available projects for the user
|
|
20
|
+
* Matching extension's fetchProjects function in api-client.js
|
|
21
|
+
*/
|
|
22
|
+
export async function fetchProjects(apiKey) {
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(`${API_BASE_URL}/api/vscode-projects`, {
|
|
25
|
+
method: 'GET',
|
|
26
|
+
headers: {
|
|
27
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const text = await response.text();
|
|
32
|
+
throw new Error(text || `HTTP ${response.status}`);
|
|
33
|
+
}
|
|
34
|
+
const data = await response.json();
|
|
35
|
+
return data.projects || [];
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error('Failed to fetch projects:', error);
|
|
39
|
+
throw new Error(error.message || 'Failed to fetch projects');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Send init scan to backend
|
|
44
|
+
* Matching extension's init-scan API call
|
|
45
|
+
*/
|
|
46
|
+
export async function sendInitScan(apiKey, projectId, payload) {
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(`${API_BASE_URL_ORCH}/api/projects/${projectId}/init-scan`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify(payload),
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const text = await response.text();
|
|
58
|
+
throw new Error(text || `HTTP ${response.status}`);
|
|
59
|
+
}
|
|
60
|
+
return await response.json();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error('Failed to send init scan:', error);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
// Global config location: ~/.shift/config.json
|
|
5
|
+
const GLOBAL_SHIFT_DIR = path.join(os.homedir(), '.shift');
|
|
6
|
+
const GLOBAL_CONFIG_FILE = path.join(GLOBAL_SHIFT_DIR, 'config.json');
|
|
7
|
+
// Local config location: .shift/ in current directory
|
|
8
|
+
const LOCAL_SHIFT_DIR = '.shift';
|
|
9
|
+
const LOCAL_CONFIG_FILE = 'config.json'; // Changed from project.json to match extension
|
|
10
|
+
const DAEMON_PID_FILE = 'daemon.pid';
|
|
11
|
+
const DAEMON_STATUS_FILE = 'daemon.status.json';
|
|
12
|
+
// Default URLs
|
|
13
|
+
const DEFAULT_API_URL = 'http://localhost:9000';
|
|
14
|
+
const DEFAULT_ORCH_URL = 'http://localhost:9999';
|
|
15
|
+
const DEFAULT_WS_URL = 'ws://localhost:9999';
|
|
16
|
+
// Helper to get URL from: 1) env var, 2) global config, 3) default
|
|
17
|
+
function getConfiguredUrl(envVar, configKey, defaultUrl) {
|
|
18
|
+
// Priority: Environment variable > Global config > Default
|
|
19
|
+
if (process.env[envVar]) {
|
|
20
|
+
return process.env[envVar];
|
|
21
|
+
}
|
|
22
|
+
const config = readGlobalConfigInternal();
|
|
23
|
+
if (config?.[configKey]) {
|
|
24
|
+
return config[configKey];
|
|
25
|
+
}
|
|
26
|
+
return defaultUrl;
|
|
27
|
+
}
|
|
28
|
+
// Internal read to avoid circular dependency
|
|
29
|
+
function readGlobalConfigInternal() {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
32
|
+
const content = fs.readFileSync(GLOBAL_CONFIG_FILE, 'utf-8');
|
|
33
|
+
return JSON.parse(content);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Invalid config, treat as not existing
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
// API URLs - Priority: env var > global config > default
|
|
42
|
+
export const API_BASE_URL = getConfiguredUrl('SHIFT_API_URL', 'api_url', DEFAULT_API_URL);
|
|
43
|
+
export const API_BASE_URL_ORCH = getConfiguredUrl('SHIFT_ORCH_URL', 'orch_url', DEFAULT_ORCH_URL);
|
|
44
|
+
export const WS_URL = getConfiguredUrl('SHIFT_WS_URL', 'ws_url', DEFAULT_WS_URL);
|
|
45
|
+
// --- Global Config Functions ---
|
|
46
|
+
export function ensureGlobalShiftDir() {
|
|
47
|
+
if (!fs.existsSync(GLOBAL_SHIFT_DIR)) {
|
|
48
|
+
fs.mkdirSync(GLOBAL_SHIFT_DIR, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function readGlobalConfig() {
|
|
52
|
+
try {
|
|
53
|
+
if (fs.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
54
|
+
const content = fs.readFileSync(GLOBAL_CONFIG_FILE, 'utf-8');
|
|
55
|
+
return JSON.parse(content);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Invalid config, treat as not existing
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
export function writeGlobalConfig(config) {
|
|
64
|
+
ensureGlobalShiftDir();
|
|
65
|
+
fs.writeFileSync(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
66
|
+
}
|
|
67
|
+
export function getApiKey() {
|
|
68
|
+
const config = readGlobalConfig();
|
|
69
|
+
return config?.api_key || null;
|
|
70
|
+
}
|
|
71
|
+
export function setApiKey(apiKey) {
|
|
72
|
+
const config = readGlobalConfig() || { api_key: '' };
|
|
73
|
+
config.api_key = apiKey;
|
|
74
|
+
delete config.key_type;
|
|
75
|
+
writeGlobalConfig(config);
|
|
76
|
+
}
|
|
77
|
+
export function setGuestKey(apiKey) {
|
|
78
|
+
const config = {
|
|
79
|
+
api_key: apiKey,
|
|
80
|
+
key_type: 'guest',
|
|
81
|
+
};
|
|
82
|
+
writeGlobalConfig(config);
|
|
83
|
+
}
|
|
84
|
+
export function isGuestKey() {
|
|
85
|
+
const config = readGlobalConfig();
|
|
86
|
+
return config?.key_type === 'guest';
|
|
87
|
+
}
|
|
88
|
+
export function clearApiKey() {
|
|
89
|
+
const config = readGlobalConfig() || { api_key: '' };
|
|
90
|
+
config.api_key = '';
|
|
91
|
+
delete config.key_type;
|
|
92
|
+
writeGlobalConfig(config);
|
|
93
|
+
}
|
|
94
|
+
// --- URL Configuration Functions ---
|
|
95
|
+
export function setApiUrl(url) {
|
|
96
|
+
const config = readGlobalConfig() || { api_key: '' };
|
|
97
|
+
config.api_url = url;
|
|
98
|
+
writeGlobalConfig(config);
|
|
99
|
+
}
|
|
100
|
+
export function setOrchUrl(url) {
|
|
101
|
+
const config = readGlobalConfig() || { api_key: '' };
|
|
102
|
+
config.orch_url = url;
|
|
103
|
+
writeGlobalConfig(config);
|
|
104
|
+
}
|
|
105
|
+
export function setWsUrl(url) {
|
|
106
|
+
const config = readGlobalConfig() || { api_key: '' };
|
|
107
|
+
config.ws_url = url;
|
|
108
|
+
writeGlobalConfig(config);
|
|
109
|
+
}
|
|
110
|
+
export function clearUrls() {
|
|
111
|
+
const config = readGlobalConfig() || { api_key: '' };
|
|
112
|
+
delete config.api_url;
|
|
113
|
+
delete config.orch_url;
|
|
114
|
+
delete config.ws_url;
|
|
115
|
+
writeGlobalConfig(config);
|
|
116
|
+
}
|
|
117
|
+
export function getConfiguredUrls() {
|
|
118
|
+
return {
|
|
119
|
+
api_url: API_BASE_URL,
|
|
120
|
+
orch_url: API_BASE_URL_ORCH,
|
|
121
|
+
ws_url: WS_URL,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// --- Local Config Functions ---
|
|
125
|
+
export function getLocalShiftDir(projectRoot) {
|
|
126
|
+
const root = projectRoot || process.cwd();
|
|
127
|
+
return path.join(root, LOCAL_SHIFT_DIR);
|
|
128
|
+
}
|
|
129
|
+
export function ensureLocalShiftDir(projectRoot) {
|
|
130
|
+
const dir = getLocalShiftDir(projectRoot);
|
|
131
|
+
if (!fs.existsSync(dir)) {
|
|
132
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export function readProjectConfig(projectRoot) {
|
|
136
|
+
try {
|
|
137
|
+
const filePath = path.join(getLocalShiftDir(projectRoot), LOCAL_CONFIG_FILE);
|
|
138
|
+
if (fs.existsSync(filePath)) {
|
|
139
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
140
|
+
return JSON.parse(content);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Invalid config, treat as not existing
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
export function writeProjectConfig(config, projectRoot) {
|
|
149
|
+
ensureLocalShiftDir(projectRoot);
|
|
150
|
+
const filePath = path.join(getLocalShiftDir(projectRoot), LOCAL_CONFIG_FILE);
|
|
151
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
|
|
152
|
+
// Also create .gitignore in .shift folder (matching extension)
|
|
153
|
+
const gitignorePath = path.join(getLocalShiftDir(projectRoot), '.gitignore');
|
|
154
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
155
|
+
fs.writeFileSync(gitignorePath, '*\n!.gitignore\n!config.json\n');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export function getProjectId(projectRoot) {
|
|
159
|
+
const config = readProjectConfig(projectRoot);
|
|
160
|
+
return config?.project_id || null;
|
|
161
|
+
}
|
|
162
|
+
export function getProjectName(projectRoot) {
|
|
163
|
+
const config = readProjectConfig(projectRoot);
|
|
164
|
+
return config?.project_name || null;
|
|
165
|
+
}
|
|
166
|
+
export function setProject(projectId, projectName, projectRoot) {
|
|
167
|
+
const existing = readProjectConfig(projectRoot);
|
|
168
|
+
const config = {
|
|
169
|
+
project_id: projectId,
|
|
170
|
+
project_name: projectName,
|
|
171
|
+
agents: existing?.agents || [],
|
|
172
|
+
created_at: existing?.created_at || new Date().toISOString(),
|
|
173
|
+
};
|
|
174
|
+
writeProjectConfig(config, projectRoot);
|
|
175
|
+
}
|
|
176
|
+
// --- Daemon Status Functions ---
|
|
177
|
+
export function getDaemonPidPath(projectRoot) {
|
|
178
|
+
return path.join(getLocalShiftDir(projectRoot), DAEMON_PID_FILE);
|
|
179
|
+
}
|
|
180
|
+
export function getDaemonStatusPath(projectRoot) {
|
|
181
|
+
return path.join(getLocalShiftDir(projectRoot), DAEMON_STATUS_FILE);
|
|
182
|
+
}
|
|
183
|
+
export function readDaemonPid(projectRoot) {
|
|
184
|
+
try {
|
|
185
|
+
const filePath = getDaemonPidPath(projectRoot);
|
|
186
|
+
if (fs.existsSync(filePath)) {
|
|
187
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
188
|
+
const pid = parseInt(content, 10);
|
|
189
|
+
return isNaN(pid) ? null : pid;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Error reading PID file
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
export function writeDaemonPid(pid, projectRoot) {
|
|
198
|
+
ensureLocalShiftDir(projectRoot);
|
|
199
|
+
const filePath = getDaemonPidPath(projectRoot);
|
|
200
|
+
fs.writeFileSync(filePath, String(pid));
|
|
201
|
+
}
|
|
202
|
+
export function removeDaemonPid(projectRoot) {
|
|
203
|
+
const filePath = getDaemonPidPath(projectRoot);
|
|
204
|
+
if (fs.existsSync(filePath)) {
|
|
205
|
+
fs.unlinkSync(filePath);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export function readDaemonStatus(projectRoot) {
|
|
209
|
+
try {
|
|
210
|
+
const filePath = getDaemonStatusPath(projectRoot);
|
|
211
|
+
if (fs.existsSync(filePath)) {
|
|
212
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
213
|
+
return JSON.parse(content);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Invalid status file
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
export function writeDaemonStatus(status, projectRoot) {
|
|
222
|
+
ensureLocalShiftDir(projectRoot);
|
|
223
|
+
const filePath = getDaemonStatusPath(projectRoot);
|
|
224
|
+
fs.writeFileSync(filePath, JSON.stringify(status, null, 2));
|
|
225
|
+
}
|
|
226
|
+
export function removeDaemonStatus(projectRoot) {
|
|
227
|
+
const filePath = getDaemonStatusPath(projectRoot);
|
|
228
|
+
if (fs.existsSync(filePath)) {
|
|
229
|
+
fs.unlinkSync(filePath);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// --- Process Check ---
|
|
233
|
+
export function isProcessRunning(pid) {
|
|
234
|
+
try {
|
|
235
|
+
// Sending signal 0 checks if process exists without killing it
|
|
236
|
+
process.kill(pid, 0);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
function createReadlineInterface() {
|
|
3
|
+
return readline.createInterface({
|
|
4
|
+
input: process.stdin,
|
|
5
|
+
output: process.stdout,
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export function prompt(question) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = createReadlineInterface();
|
|
11
|
+
rl.question(question, (answer) => {
|
|
12
|
+
rl.close();
|
|
13
|
+
resolve(answer.trim());
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export async function promptApiKey() {
|
|
18
|
+
console.log('\nNo Shift API key found.');
|
|
19
|
+
const apiKey = await prompt('Please paste your Shift API key: ');
|
|
20
|
+
if (!apiKey) {
|
|
21
|
+
console.error('Error: API key is required.');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
return apiKey;
|
|
25
|
+
}
|
|
26
|
+
export async function promptProjectId() {
|
|
27
|
+
console.log('\nNo project configured for this directory.');
|
|
28
|
+
const projectId = await prompt('Please enter your Shift project ID: ');
|
|
29
|
+
if (!projectId) {
|
|
30
|
+
console.error('Error: Project ID is required.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
return projectId;
|
|
34
|
+
}
|
|
35
|
+
export async function promptSelectProject(projects) {
|
|
36
|
+
console.log('\nAvailable projects:');
|
|
37
|
+
console.log('-------------------');
|
|
38
|
+
projects.forEach((project, index) => {
|
|
39
|
+
const description = project.description ? ` - ${project.description}` : '';
|
|
40
|
+
console.log(` ${index + 1}. ${project.project_name}${description}`);
|
|
41
|
+
});
|
|
42
|
+
console.log('');
|
|
43
|
+
const answer = await prompt(`Select a project (1-${projects.length}): `);
|
|
44
|
+
const selection = parseInt(answer, 10);
|
|
45
|
+
if (isNaN(selection) || selection < 1 || selection > projects.length) {
|
|
46
|
+
console.error('Invalid selection.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return projects[selection - 1];
|
|
50
|
+
}
|
|
51
|
+
export async function promptKeyChoice() {
|
|
52
|
+
console.log('\nNo Shift API key found.');
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(' 1. I have an API key');
|
|
55
|
+
console.log(' 2. Continue as guest');
|
|
56
|
+
console.log('');
|
|
57
|
+
const answer = await prompt('Select an option (1 or 2): ');
|
|
58
|
+
const selection = parseInt(answer, 10);
|
|
59
|
+
if (selection === 1) {
|
|
60
|
+
return 'paid';
|
|
61
|
+
}
|
|
62
|
+
else if (selection === 2) {
|
|
63
|
+
return 'guest';
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.error('Invalid selection.');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
// Matching extension's file-tools.js exclude patterns
|
|
4
|
+
const DEFAULT_EXCLUDE_PATTERNS = [
|
|
5
|
+
'.git',
|
|
6
|
+
'node_modules',
|
|
7
|
+
'__pycache__',
|
|
8
|
+
'.vscode',
|
|
9
|
+
'dist',
|
|
10
|
+
'build',
|
|
11
|
+
'.shift',
|
|
12
|
+
'.next',
|
|
13
|
+
'.cache',
|
|
14
|
+
'coverage',
|
|
15
|
+
'.pytest_cache',
|
|
16
|
+
'venv',
|
|
17
|
+
'env',
|
|
18
|
+
'.env',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Get project tree - matching extension's getProjectTree function
|
|
22
|
+
*/
|
|
23
|
+
export function getProjectTree(workspaceRoot, options = {}) {
|
|
24
|
+
const { depth = 0, exclude_patterns = DEFAULT_EXCLUDE_PATTERNS, } = options;
|
|
25
|
+
let file_count = 0;
|
|
26
|
+
let dir_count = 0;
|
|
27
|
+
let total_size = 0;
|
|
28
|
+
const max_depth = depth === 0 ? Infinity : depth;
|
|
29
|
+
function scanDirectory(dirPath, currentDepth, relativePath) {
|
|
30
|
+
if (currentDepth >= max_depth) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const items = [];
|
|
34
|
+
try {
|
|
35
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
// Check if should be excluded
|
|
38
|
+
if (exclude_patterns.some(pattern => entry.name.includes(pattern))) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const itemPath = path.join(dirPath, entry.name);
|
|
42
|
+
const itemRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
dir_count++;
|
|
45
|
+
const children = scanDirectory(itemPath, currentDepth + 1, itemRelativePath);
|
|
46
|
+
items.push({
|
|
47
|
+
name: entry.name,
|
|
48
|
+
type: 'directory',
|
|
49
|
+
path: itemRelativePath.replace(/\\/g, '/'), // Normalize to forward slashes
|
|
50
|
+
depth: currentDepth,
|
|
51
|
+
children: children,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else if (entry.isFile()) {
|
|
55
|
+
file_count++;
|
|
56
|
+
try {
|
|
57
|
+
const stats = fs.statSync(itemPath);
|
|
58
|
+
total_size += stats.size;
|
|
59
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
60
|
+
items.push({
|
|
61
|
+
name: entry.name,
|
|
62
|
+
type: 'file',
|
|
63
|
+
path: itemRelativePath.replace(/\\/g, '/'), // Normalize to forward slashes
|
|
64
|
+
depth: currentDepth,
|
|
65
|
+
size: stats.size,
|
|
66
|
+
extension: ext,
|
|
67
|
+
modified: stats.mtime,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Skip files we can't read
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error(`Error scanning ${dirPath}:`, err.message);
|
|
78
|
+
}
|
|
79
|
+
return items;
|
|
80
|
+
}
|
|
81
|
+
const tree = scanDirectory(workspaceRoot, 0, '');
|
|
82
|
+
const total_size_mb = (total_size / (1024 * 1024)).toFixed(2);
|
|
83
|
+
return {
|
|
84
|
+
status: 'success',
|
|
85
|
+
tree: tree,
|
|
86
|
+
file_count: file_count,
|
|
87
|
+
dir_count: dir_count,
|
|
88
|
+
total_size_bytes: total_size,
|
|
89
|
+
total_size_mb: total_size_mb,
|
|
90
|
+
scanned_from: workspaceRoot,
|
|
91
|
+
depth_limit: depth,
|
|
92
|
+
actual_max_depth: depth === 0 ? 'unlimited' : String(depth),
|
|
93
|
+
excluded_patterns: exclude_patterns,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Extract all file paths from tree structure
|
|
98
|
+
* Matching extension's extractAllFilePaths function
|
|
99
|
+
*/
|
|
100
|
+
export function extractAllFilePaths(tree, basePath = '') {
|
|
101
|
+
let files = [];
|
|
102
|
+
for (const item of tree) {
|
|
103
|
+
const itemPath = basePath ? `${basePath}/${item.name}` : item.name;
|
|
104
|
+
if (item.type === 'file') {
|
|
105
|
+
files.push(itemPath);
|
|
106
|
+
}
|
|
107
|
+
else if (item.type === 'directory' && item.children) {
|
|
108
|
+
files = files.concat(extractAllFilePaths(item.children, itemPath));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return files;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Categorize files by type
|
|
115
|
+
* Matching extension's categorizeFiles function
|
|
116
|
+
*/
|
|
117
|
+
export function categorizeFiles(tree, basePath = '') {
|
|
118
|
+
const categories = {
|
|
119
|
+
source_files: [],
|
|
120
|
+
config_files: [],
|
|
121
|
+
asset_files: [],
|
|
122
|
+
};
|
|
123
|
+
const sourceExts = ['.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.cpp', '.cs', '.go', '.c', '.h'];
|
|
124
|
+
const configExts = ['.json', '.yaml', '.yml', '.toml', '.ini', '.config', '.xml'];
|
|
125
|
+
const assetExts = ['.png', '.jpg', '.jpeg', '.svg', '.gif', '.css', '.scss', '.less', '.sass'];
|
|
126
|
+
for (const item of tree) {
|
|
127
|
+
const itemPath = basePath ? `${basePath}/${item.name}` : item.name;
|
|
128
|
+
if (item.type === 'file') {
|
|
129
|
+
const ext = path.extname(item.name).toLowerCase();
|
|
130
|
+
if (sourceExts.includes(ext)) {
|
|
131
|
+
categories.source_files.push(itemPath);
|
|
132
|
+
}
|
|
133
|
+
else if (configExts.includes(ext)) {
|
|
134
|
+
categories.config_files.push(itemPath);
|
|
135
|
+
}
|
|
136
|
+
else if (assetExts.includes(ext)) {
|
|
137
|
+
categories.asset_files.push(itemPath);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (item.type === 'directory' && item.children) {
|
|
141
|
+
const subCategories = categorizeFiles(item.children, itemPath);
|
|
142
|
+
categories.source_files.push(...subCategories.source_files);
|
|
143
|
+
categories.config_files.push(...subCategories.config_files);
|
|
144
|
+
categories.asset_files.push(...subCategories.asset_files);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return categories;
|
|
148
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@latentforce/shift",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Shift CLI - AI-powered code intelligence with MCP support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./build/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -37,10 +37,13 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
40
|
+
"commander": "^12.0.0",
|
|
41
|
+
"ws": "^8.14.0",
|
|
40
42
|
"zod": "^3.25.76"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
45
|
"@types/node": "^25.0.9",
|
|
46
|
+
"@types/ws": "^8.5.10",
|
|
44
47
|
"typescript": "^5.9.3"
|
|
45
48
|
}
|
|
46
49
|
}
|