@latentforce/latentgraph 1.0.0
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 +320 -0
- package/build/cli/commands/add.js +350 -0
- package/build/cli/commands/config.js +142 -0
- package/build/cli/commands/init.js +285 -0
- package/build/cli/commands/start.js +65 -0
- package/build/cli/commands/status.js +76 -0
- package/build/cli/commands/stop.js +18 -0
- package/build/cli/commands/update-drg.js +194 -0
- package/build/daemon/daemon-manager.js +136 -0
- package/build/daemon/daemon.js +119 -0
- package/build/daemon/tools-executor.js +462 -0
- package/build/daemon/websocket-client.js +334 -0
- package/build/index.js +205 -0
- package/build/mcp-server.js +484 -0
- package/build/utils/api-client.js +147 -0
- package/build/utils/auth-resolver.js +184 -0
- package/build/utils/config.js +260 -0
- package/build/utils/machine-id.js +46 -0
- package/build/utils/prompts.js +114 -0
- package/build/utils/shiftignore.js +108 -0
- package/build/utils/tree-scanner.js +165 -0
- package/package.json +49 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { WS_URL } from '../utils/config.js';
|
|
4
|
+
import { ToolsExecutor } from './tools-executor.js';
|
|
5
|
+
const RECONNECT_INTERVAL = 5000; // 5 seconds - matching extension
|
|
6
|
+
const MAX_RECONNECT_ATTEMPTS = 100; // matching extension
|
|
7
|
+
/**
|
|
8
|
+
* WebSocket client matching extension's websocket-client.js
|
|
9
|
+
*/
|
|
10
|
+
export class WebSocketClient extends EventEmitter {
|
|
11
|
+
ws = null;
|
|
12
|
+
authenticated = false;
|
|
13
|
+
projectInfo = null;
|
|
14
|
+
userInfo = null;
|
|
15
|
+
toolsExecutor = null;
|
|
16
|
+
shouldReconnect = true;
|
|
17
|
+
reconnectTimer = null;
|
|
18
|
+
apiKey;
|
|
19
|
+
projectId;
|
|
20
|
+
wsUrl;
|
|
21
|
+
isConnecting = false;
|
|
22
|
+
reconnectAttempts = 0;
|
|
23
|
+
maxReconnectAttempts = MAX_RECONNECT_ATTEMPTS;
|
|
24
|
+
workspaceRoot;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
super();
|
|
27
|
+
this.apiKey = options.apiKey;
|
|
28
|
+
this.projectId = options.projectId;
|
|
29
|
+
this.wsUrl = options.wsUrl || WS_URL;
|
|
30
|
+
this.workspaceRoot = options.workspaceRoot || process.cwd();
|
|
31
|
+
// Initialize tools executor
|
|
32
|
+
this.toolsExecutor = new ToolsExecutor(this, this.workspaceRoot);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Set the tools executor
|
|
36
|
+
*/
|
|
37
|
+
setToolsExecutor(toolsExecutor) {
|
|
38
|
+
this.toolsExecutor = toolsExecutor;
|
|
39
|
+
console.log('[WS-Client] Tools executor set');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Connect to WebSocket server
|
|
43
|
+
*/
|
|
44
|
+
async connect() {
|
|
45
|
+
if (!this.apiKey || !this.projectId) {
|
|
46
|
+
throw new Error('API key and project ID are required');
|
|
47
|
+
}
|
|
48
|
+
this.shouldReconnect = true;
|
|
49
|
+
this.reconnectAttempts = 0;
|
|
50
|
+
console.log(`[WS-Client] Initiating connection to project: ${this.projectId}`);
|
|
51
|
+
return this._attemptConnection();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Internal method to attempt connection
|
|
55
|
+
*/
|
|
56
|
+
async _attemptConnection() {
|
|
57
|
+
if (this.isConnecting) {
|
|
58
|
+
console.log('[WS-Client] Connection attempt already in progress, skipping...');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
62
|
+
console.error(`[WS-Client] Max reconnection attempts (${this.maxReconnectAttempts}) reached. Giving up.`);
|
|
63
|
+
this.emit('max_reconnects_reached');
|
|
64
|
+
this.shouldReconnect = false;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.isConnecting = true;
|
|
68
|
+
this.reconnectAttempts++;
|
|
69
|
+
// Emit reconnecting event
|
|
70
|
+
this.emit('reconnecting', this.reconnectAttempts);
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
// IMPORTANT: Use project_id in URL, matching extension
|
|
73
|
+
const wsUrl = `${this.wsUrl}/ws/extension/${this.projectId}`;
|
|
74
|
+
console.log(`[WS-Client] Connecting to ${wsUrl} (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
75
|
+
try {
|
|
76
|
+
this.ws = new WebSocket(wsUrl);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error('[WS-Client] Failed to create WebSocket:', error);
|
|
80
|
+
this.isConnecting = false;
|
|
81
|
+
this._scheduleReconnect();
|
|
82
|
+
reject(error);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Connection timeout (10 seconds) - matching extension
|
|
86
|
+
const timeout = setTimeout(() => {
|
|
87
|
+
console.log('[WS-Client] Connection timeout (10s)');
|
|
88
|
+
if (this.ws) {
|
|
89
|
+
this.ws.close();
|
|
90
|
+
}
|
|
91
|
+
this.isConnecting = false;
|
|
92
|
+
this._scheduleReconnect();
|
|
93
|
+
reject(new Error('Connection timeout'));
|
|
94
|
+
}, 10000);
|
|
95
|
+
// WebSocket opened - send auth
|
|
96
|
+
this.ws.on('open', () => {
|
|
97
|
+
console.log('[WS-Client] ✓ WebSocket connection opened');
|
|
98
|
+
this.emit('connecting');
|
|
99
|
+
// Send authentication message - matching extension format
|
|
100
|
+
const authMessage = {
|
|
101
|
+
type: 'auth',
|
|
102
|
+
api_key: this.apiKey,
|
|
103
|
+
project_id: this.projectId,
|
|
104
|
+
};
|
|
105
|
+
console.log('[WS-Client] Sending authentication...');
|
|
106
|
+
this.ws.send(JSON.stringify(authMessage));
|
|
107
|
+
});
|
|
108
|
+
// Message received
|
|
109
|
+
this.ws.on('message', (data) => {
|
|
110
|
+
try {
|
|
111
|
+
const message = JSON.parse(data.toString());
|
|
112
|
+
if (message.type === 'auth_success') {
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
this.authenticated = true;
|
|
115
|
+
this.userInfo = message.user;
|
|
116
|
+
this.projectInfo = message.project;
|
|
117
|
+
this.isConnecting = false;
|
|
118
|
+
this.reconnectAttempts = 0;
|
|
119
|
+
console.log('[WS-Client] ✓✓✓ AUTHENTICATION SUCCESSFUL ✓✓✓');
|
|
120
|
+
if (this.userInfo) {
|
|
121
|
+
console.log(`[WS-Client] User: ${this.userInfo.email}`);
|
|
122
|
+
}
|
|
123
|
+
if (this.projectInfo) {
|
|
124
|
+
console.log(`[WS-Client] Project: ${this.projectInfo.project_name}`);
|
|
125
|
+
}
|
|
126
|
+
if (this.reconnectTimer) {
|
|
127
|
+
clearTimeout(this.reconnectTimer);
|
|
128
|
+
this.reconnectTimer = null;
|
|
129
|
+
}
|
|
130
|
+
// Emit connected event
|
|
131
|
+
this.emit('connected', this.projectInfo);
|
|
132
|
+
resolve(message);
|
|
133
|
+
}
|
|
134
|
+
else if (message.type === 'auth_failed') {
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
console.error('[WS-Client] ❌ AUTHENTICATION FAILED:', message.message);
|
|
137
|
+
this.isConnecting = false;
|
|
138
|
+
this.shouldReconnect = false; // Don't retry on auth failure
|
|
139
|
+
this.emit('auth_failed', message.message);
|
|
140
|
+
reject(new Error(message.message));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Handle other messages
|
|
144
|
+
this.handleMessage(message);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.error('[WS-Client] Error parsing message:', err);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
// WebSocket error
|
|
152
|
+
this.ws.on('error', (error) => {
|
|
153
|
+
clearTimeout(timeout);
|
|
154
|
+
console.error('[WS-Client] WebSocket error:', error.message);
|
|
155
|
+
this.isConnecting = false;
|
|
156
|
+
this.emit('error', error);
|
|
157
|
+
this._scheduleReconnect();
|
|
158
|
+
reject(error);
|
|
159
|
+
});
|
|
160
|
+
// WebSocket closed
|
|
161
|
+
this.ws.on('close', (code, reason) => {
|
|
162
|
+
clearTimeout(timeout);
|
|
163
|
+
console.log(`[WS-Client] Connection closed (code: ${code}, reason: ${reason?.toString() || 'none'})`);
|
|
164
|
+
this.authenticated = false;
|
|
165
|
+
this.isConnecting = false;
|
|
166
|
+
this.emit('disconnected', code, reason?.toString());
|
|
167
|
+
if (this.shouldReconnect) {
|
|
168
|
+
this._scheduleReconnect();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Schedule reconnection attempt
|
|
175
|
+
*/
|
|
176
|
+
_scheduleReconnect() {
|
|
177
|
+
if (this.reconnectTimer || !this.shouldReconnect) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
console.log(`[WS-Client] Scheduling reconnect in ${RECONNECT_INTERVAL}ms... (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
181
|
+
this.reconnectTimer = setTimeout(() => {
|
|
182
|
+
this.reconnectTimer = null;
|
|
183
|
+
console.log('[WS-Client] Attempting to reconnect...');
|
|
184
|
+
this._attemptConnection().catch(err => {
|
|
185
|
+
console.error('[WS-Client] Reconnection failed:', err.message);
|
|
186
|
+
});
|
|
187
|
+
}, RECONNECT_INTERVAL);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Handle incoming messages
|
|
191
|
+
*/
|
|
192
|
+
async handleMessage(message) {
|
|
193
|
+
console.log(`[WS-Client] ← Received: ${message.type}`);
|
|
194
|
+
if (message.type === 'execute_tool') {
|
|
195
|
+
await this.handleToolRequest(message);
|
|
196
|
+
}
|
|
197
|
+
else if (message.type === 'tool_ack') {
|
|
198
|
+
console.log(`[WS-Client] Server acknowledged tool: ${message.tool} (${message.request_id})`);
|
|
199
|
+
this.emit('tool_acknowledged', message);
|
|
200
|
+
}
|
|
201
|
+
else if (message.type === 'pong') {
|
|
202
|
+
console.log('[WS-Client] Pong received');
|
|
203
|
+
this.emit('pong');
|
|
204
|
+
}
|
|
205
|
+
else if (message.type === 'error') {
|
|
206
|
+
console.error('[WS-Client] Server error:', message.message);
|
|
207
|
+
this.emit('server_error', message.message);
|
|
208
|
+
}
|
|
209
|
+
else if (message.type === 'user_input_required') {
|
|
210
|
+
console.log('[WS-Client] 🔔 User input required notification received');
|
|
211
|
+
console.log(`[WS-Client] Agent: ${message.agent_name}`);
|
|
212
|
+
console.log(`[WS-Client] Questions: ${message.questions_count}`);
|
|
213
|
+
this.emit('user_input_required', {
|
|
214
|
+
agent_id: message.agent_id,
|
|
215
|
+
agent_name: message.agent_name,
|
|
216
|
+
questions_count: message.questions_count,
|
|
217
|
+
message: message.message
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.log('[WS-Client] Unknown message type:', message.type);
|
|
222
|
+
this.emit('message', message);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Handle tool execution request from server
|
|
227
|
+
*/
|
|
228
|
+
async handleToolRequest(message) {
|
|
229
|
+
const toolName = message.tool;
|
|
230
|
+
const params = message.params || {};
|
|
231
|
+
const requestId = message.request_id;
|
|
232
|
+
console.log(`[WS-Client] ═══════════════════════════════════`);
|
|
233
|
+
console.log(`[WS-Client] TOOL EXECUTION REQUEST`);
|
|
234
|
+
console.log(`[WS-Client] Tool: ${toolName}`);
|
|
235
|
+
console.log(`[WS-Client] Request ID: ${requestId}`);
|
|
236
|
+
console.log(`[WS-Client] Params:`, JSON.stringify(params, null, 2));
|
|
237
|
+
console.log(`[WS-Client] ═══════════════════════════════════`);
|
|
238
|
+
if (!this.toolsExecutor) {
|
|
239
|
+
console.error('[WS-Client] ❌ Tools executor not available');
|
|
240
|
+
await this.sendMessage({
|
|
241
|
+
type: 'tool_error',
|
|
242
|
+
tool: toolName,
|
|
243
|
+
request_id: requestId,
|
|
244
|
+
error: 'Tools executor not initialized'
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
console.log(`[WS-Client] Executing tool: ${toolName}...`);
|
|
250
|
+
const result = await this.toolsExecutor.executeTool(toolName, params);
|
|
251
|
+
console.log(`[WS-Client] ✓ Tool execution completed`);
|
|
252
|
+
console.log(`[WS-Client] Result status: ${result?.status || 'unknown'}`);
|
|
253
|
+
await this.sendMessage({
|
|
254
|
+
type: 'tool_result',
|
|
255
|
+
tool: toolName,
|
|
256
|
+
result: result,
|
|
257
|
+
request_id: requestId,
|
|
258
|
+
timestamp: new Date().toISOString()
|
|
259
|
+
});
|
|
260
|
+
console.log(`[WS-Client] → Tool result sent to server`);
|
|
261
|
+
this.emit('tool_executed', { tool: toolName, result, requestId });
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
console.error(`[WS-Client] ❌ Tool execution failed:`, error);
|
|
265
|
+
await this.sendMessage({
|
|
266
|
+
type: 'tool_error',
|
|
267
|
+
tool: toolName,
|
|
268
|
+
request_id: requestId,
|
|
269
|
+
error: error.message,
|
|
270
|
+
timestamp: new Date().toISOString()
|
|
271
|
+
});
|
|
272
|
+
this.emit('tool_error', { tool: toolName, error, requestId });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Send a message to the server
|
|
277
|
+
*/
|
|
278
|
+
async sendMessage(message) {
|
|
279
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
280
|
+
throw new Error('WebSocket not connected');
|
|
281
|
+
}
|
|
282
|
+
this.ws.send(JSON.stringify(message));
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Send a ping to keep connection alive
|
|
286
|
+
*/
|
|
287
|
+
async ping() {
|
|
288
|
+
if (this.isConnected()) {
|
|
289
|
+
await this.sendMessage({ type: 'ping' });
|
|
290
|
+
console.log('[WS-Client] → Sent: ping');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Check if connected and authenticated
|
|
295
|
+
*/
|
|
296
|
+
isConnected() {
|
|
297
|
+
return this.authenticated && this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Disconnect from server
|
|
301
|
+
*/
|
|
302
|
+
disconnect() {
|
|
303
|
+
console.log('[WS-Client] Manual disconnect requested');
|
|
304
|
+
this.shouldReconnect = false;
|
|
305
|
+
if (this.reconnectTimer) {
|
|
306
|
+
clearTimeout(this.reconnectTimer);
|
|
307
|
+
this.reconnectTimer = null;
|
|
308
|
+
}
|
|
309
|
+
if (this.ws) {
|
|
310
|
+
this.ws.close();
|
|
311
|
+
this.ws = null;
|
|
312
|
+
}
|
|
313
|
+
this.authenticated = false;
|
|
314
|
+
this.emit('disconnected', 1000, 'Manual disconnect');
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Get project information
|
|
318
|
+
*/
|
|
319
|
+
getProjectInfo() {
|
|
320
|
+
return this.projectInfo;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Get user information
|
|
324
|
+
*/
|
|
325
|
+
getUserInfo() {
|
|
326
|
+
return this.userInfo;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get current reconnection attempt count
|
|
330
|
+
*/
|
|
331
|
+
getReconnectAttempts() {
|
|
332
|
+
return this.reconnectAttempts;
|
|
333
|
+
}
|
|
334
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const { version } = require('../package.json');
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program
|
|
8
|
+
.name('lgraph')
|
|
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
|
+
' lgraph start Start the daemon and configure project\n' +
|
|
14
|
+
' lgraph init Scan and index the project\n' +
|
|
15
|
+
' lgraph update-drg Build/update the dependency graph\n' +
|
|
16
|
+
' lgraph status Check current status\n\n' +
|
|
17
|
+
'Configuration:\n' +
|
|
18
|
+
' Global config: ~/.shift/config.json\n' +
|
|
19
|
+
' Project config: .shift/config.json')
|
|
20
|
+
.version(version);
|
|
21
|
+
// MCP server mode (default when run via MCP host)
|
|
22
|
+
program
|
|
23
|
+
.command('mcp', { isDefault: true, hidden: true })
|
|
24
|
+
.description('Start MCP server on stdio')
|
|
25
|
+
.action(async () => {
|
|
26
|
+
const mcpExplicitlyRequested = process.argv.includes('mcp');
|
|
27
|
+
if (process.stdin.isTTY && !mcpExplicitlyRequested) {
|
|
28
|
+
program.outputHelp();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const { startMcpServer } = await import('./mcp-server.js');
|
|
32
|
+
await startMcpServer();
|
|
33
|
+
});
|
|
34
|
+
// --- start ---
|
|
35
|
+
const startCmd = program
|
|
36
|
+
.command('start')
|
|
37
|
+
.description('Start the Shift daemon for this project')
|
|
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')
|
|
43
|
+
.action(async (options) => {
|
|
44
|
+
const { startCommand } = await import('./cli/commands/start.js');
|
|
45
|
+
await startCommand({
|
|
46
|
+
guest: options.guest,
|
|
47
|
+
apiKey: options.apiKey,
|
|
48
|
+
projectName: options.projectName,
|
|
49
|
+
projectId: options.projectId,
|
|
50
|
+
template: options.template,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
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
|
+
lgraph start Interactive setup
|
|
61
|
+
lgraph start --guest Quick start without API key
|
|
62
|
+
lgraph start --api-key <key> --project-name "My App"
|
|
63
|
+
`);
|
|
64
|
+
// --- init ---
|
|
65
|
+
const initCmd = program
|
|
66
|
+
.command('init')
|
|
67
|
+
.description('Initialize and scan the project for file indexing')
|
|
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')
|
|
74
|
+
.action(async (options) => {
|
|
75
|
+
const { initCommand } = await import('./cli/commands/init.js');
|
|
76
|
+
await initCommand({
|
|
77
|
+
force: options.force ?? false,
|
|
78
|
+
guest: options.guest,
|
|
79
|
+
apiKey: options.apiKey,
|
|
80
|
+
projectName: options.projectName,
|
|
81
|
+
projectId: options.projectId,
|
|
82
|
+
template: options.template,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
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
|
+
lgraph init Interactive initialization
|
|
96
|
+
lgraph init --force Force re-index without prompt
|
|
97
|
+
lgraph init --guest Quick init with guest auth
|
|
98
|
+
`);
|
|
99
|
+
// --- stop ---
|
|
100
|
+
const stopCmd = program
|
|
101
|
+
.command('stop')
|
|
102
|
+
.description('Stop the Shift daemon for this project')
|
|
103
|
+
.action(async () => {
|
|
104
|
+
const { stopCommand } = await import('./cli/commands/stop.js');
|
|
105
|
+
await stopCommand();
|
|
106
|
+
});
|
|
107
|
+
stopCmd.addHelpText('after', `
|
|
108
|
+
Details:
|
|
109
|
+
Terminates the background daemon process. The daemon can be
|
|
110
|
+
restarted later with "lgraph start".
|
|
111
|
+
`);
|
|
112
|
+
// --- status ---
|
|
113
|
+
const statusCmd = program
|
|
114
|
+
.command('status')
|
|
115
|
+
.description('Show the current Shift status')
|
|
116
|
+
.action(async () => {
|
|
117
|
+
const { statusCommand } = await import('./cli/commands/status.js');
|
|
118
|
+
await statusCommand();
|
|
119
|
+
});
|
|
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
|
|
131
|
+
.command('update-drg')
|
|
132
|
+
.description('Update the dependency relationship graph (DRG)')
|
|
133
|
+
.option('-m, --mode <mode>', 'Update mode: "baseline" (all files) or "incremental" (git-changed only)', 'incremental')
|
|
134
|
+
.action(async (options) => {
|
|
135
|
+
const { updateDrgCommand } = await import('./cli/commands/update-drg.js');
|
|
136
|
+
await updateDrgCommand({ mode: options.mode });
|
|
137
|
+
});
|
|
138
|
+
updateDrgCmd.addHelpText('after', `
|
|
139
|
+
Details:
|
|
140
|
+
Scans files, detects git changes,
|
|
141
|
+
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
|
+
lgraph update-drg Incremental update
|
|
150
|
+
lgraph 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
|
+
shiftcode Write to shiftcode.json
|
|
163
|
+
claude-code Configure via Claude Code CLI
|
|
164
|
+
opencode Write to opencode.json
|
|
165
|
+
codex Configure via Codex CLI
|
|
166
|
+
copilot Write to .vscode/mcp.json
|
|
167
|
+
droid Configure via Factory-Droid CLI
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
lgraph add shiftcode
|
|
171
|
+
lgraph add claude-code
|
|
172
|
+
lgraph add copilot
|
|
173
|
+
lgraph add opencode
|
|
174
|
+
`);
|
|
175
|
+
// --- config ---
|
|
176
|
+
const configCmd = program
|
|
177
|
+
.command('config [action] [key] [value]')
|
|
178
|
+
.description('Manage Shift configuration (URLs, API key)')
|
|
179
|
+
.action(async (action, key, value) => {
|
|
180
|
+
const { configCommand } = await import('./cli/commands/config.js');
|
|
181
|
+
await configCommand(action, key, value);
|
|
182
|
+
});
|
|
183
|
+
configCmd.addHelpText('after', `
|
|
184
|
+
Actions:
|
|
185
|
+
show Display current configuration (default)
|
|
186
|
+
set <key> <val> Set a configuration value
|
|
187
|
+
clear [key] Clear a specific key or all configuration
|
|
188
|
+
|
|
189
|
+
Configurable keys:
|
|
190
|
+
api-key Your Shift API key
|
|
191
|
+
api-url Backend API URL
|
|
192
|
+
orch-url Orchestrator URL
|
|
193
|
+
ws-url WebSocket URL
|
|
194
|
+
|
|
195
|
+
URLs can also be set via environment variables:
|
|
196
|
+
SHIFT_API_URL, SHIFT_ORCH_URL, SHIFT_WS_URL
|
|
197
|
+
|
|
198
|
+
Examples:
|
|
199
|
+
lgraph config Show config
|
|
200
|
+
lgraph config set api-key sk-abc123 Set API key
|
|
201
|
+
lgraph config set api-url http://localhost:9000 Set API URL
|
|
202
|
+
lgraph config clear api-key Clear API key
|
|
203
|
+
lgraph config clear Clear all config
|
|
204
|
+
`);
|
|
205
|
+
program.parse();
|