@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,51 @@
|
|
|
1
|
+
import { getApiKey, readProjectConfig, isGuestKey } from '../../utils/config.js';
|
|
2
|
+
import { getDaemonStatus } from '../../daemon/daemon-manager.js';
|
|
3
|
+
export async function statusCommand() {
|
|
4
|
+
const projectRoot = process.cwd();
|
|
5
|
+
console.log('\n╔════════════════════════════════════════════╗');
|
|
6
|
+
console.log('║ Shift Status ║');
|
|
7
|
+
console.log('╚════════════════════════════════════════════╝\n');
|
|
8
|
+
// Check API key
|
|
9
|
+
const apiKey = getApiKey();
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
console.log('API Key: ❌ Not configured');
|
|
12
|
+
console.log('\nRun "shift start" to configure your API key.\n');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (isGuestKey()) {
|
|
16
|
+
console.log('API Key: ✓ Guest key');
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.log('API Key: ✓ Configured');
|
|
20
|
+
}
|
|
21
|
+
// Check project config
|
|
22
|
+
const projectConfig = readProjectConfig(projectRoot);
|
|
23
|
+
if (!projectConfig) {
|
|
24
|
+
console.log('Project: ❌ Not configured');
|
|
25
|
+
console.log('\nRun "shift start" to configure this project.\n');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(`Project: ${projectConfig.project_name}`);
|
|
29
|
+
console.log(`Project ID: ${projectConfig.project_id}`);
|
|
30
|
+
// Check agents
|
|
31
|
+
if (projectConfig.agents && projectConfig.agents.length > 0) {
|
|
32
|
+
console.log(`Agents: ${projectConfig.agents.length} configured`);
|
|
33
|
+
projectConfig.agents.forEach((agent) => {
|
|
34
|
+
console.log(` - ${agent.agent_name} (${agent.agent_type})`);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// Check daemon status
|
|
38
|
+
const status = getDaemonStatus(projectRoot);
|
|
39
|
+
if (!status.running) {
|
|
40
|
+
console.log('Daemon: ❌ Not running');
|
|
41
|
+
console.log('\nRun "shift start" to start the daemon.\n');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
console.log(`Daemon: ✓ Running (PID: ${status.pid})`);
|
|
45
|
+
console.log(`WebSocket: ${status.connected ? '✓ Connected' : '⚠️ Disconnected'}`);
|
|
46
|
+
if (status.startedAt) {
|
|
47
|
+
const startedAt = new Date(status.startedAt);
|
|
48
|
+
console.log(`Started: ${startedAt.toLocaleString()}`);
|
|
49
|
+
}
|
|
50
|
+
console.log('');
|
|
51
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getDaemonStatus, stopDaemon } from '../../daemon/daemon-manager.js';
|
|
2
|
+
export async function stopCommand() {
|
|
3
|
+
const projectRoot = process.cwd();
|
|
4
|
+
console.log('\nStopping Shift daemon...\n');
|
|
5
|
+
// Check if daemon is running
|
|
6
|
+
const status = getDaemonStatus(projectRoot);
|
|
7
|
+
if (!status.running) {
|
|
8
|
+
console.log('Daemon is not running.');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
console.log(`Stopping daemon (PID: ${status.pid})...`);
|
|
12
|
+
const result = await stopDaemon(projectRoot);
|
|
13
|
+
if (!result.success) {
|
|
14
|
+
console.error(`Failed to stop daemon: ${result.error}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
console.log('Daemon stopped successfully.\n');
|
|
18
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { readDaemonPid, writeDaemonPid, removeDaemonPid, removeDaemonStatus, readDaemonStatus, isProcessRunning, } from '../utils/config.js';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
function getDaemonScriptPath() {
|
|
9
|
+
// The daemon.js will be in the same directory as daemon-manager.js after compilation
|
|
10
|
+
return path.join(__dirname, 'daemon.js');
|
|
11
|
+
}
|
|
12
|
+
export async function startDaemon(projectRoot, projectId, apiKey) {
|
|
13
|
+
// Check if daemon is already running
|
|
14
|
+
const existingPid = readDaemonPid(projectRoot);
|
|
15
|
+
if (existingPid && isProcessRunning(existingPid)) {
|
|
16
|
+
return {
|
|
17
|
+
success: false,
|
|
18
|
+
error: `Daemon already running with PID ${existingPid}`,
|
|
19
|
+
pid: existingPid,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// Clean up stale files if process is not running
|
|
23
|
+
if (existingPid) {
|
|
24
|
+
removeDaemonPid(projectRoot);
|
|
25
|
+
removeDaemonStatus(projectRoot);
|
|
26
|
+
}
|
|
27
|
+
const daemonScript = getDaemonScriptPath();
|
|
28
|
+
if (!fs.existsSync(daemonScript)) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: `Daemon script not found at ${daemonScript}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
// Spawn detached daemon process
|
|
36
|
+
const child = spawn(process.execPath, [daemonScript, projectRoot, projectId, apiKey], {
|
|
37
|
+
detached: true,
|
|
38
|
+
stdio: 'ignore',
|
|
39
|
+
cwd: projectRoot,
|
|
40
|
+
});
|
|
41
|
+
if (!child.pid) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
error: 'Failed to spawn daemon process',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Detach from parent
|
|
48
|
+
child.unref();
|
|
49
|
+
// Write PID file
|
|
50
|
+
writeDaemonPid(child.pid, projectRoot);
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
pid: child.pid,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: `Failed to start daemon: ${error.message}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function stopDaemon(projectRoot) {
|
|
64
|
+
const pid = readDaemonPid(projectRoot);
|
|
65
|
+
if (!pid) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: 'No daemon PID file found',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (!isProcessRunning(pid)) {
|
|
72
|
+
// Clean up stale files
|
|
73
|
+
removeDaemonPid(projectRoot);
|
|
74
|
+
removeDaemonStatus(projectRoot);
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
// Send SIGTERM for graceful shutdown
|
|
81
|
+
process.kill(pid, 'SIGTERM');
|
|
82
|
+
// Wait for process to exit (with timeout)
|
|
83
|
+
const maxWait = 5000;
|
|
84
|
+
const checkInterval = 100;
|
|
85
|
+
let waited = 0;
|
|
86
|
+
while (waited < maxWait && isProcessRunning(pid)) {
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
88
|
+
waited += checkInterval;
|
|
89
|
+
}
|
|
90
|
+
// If still running, force kill
|
|
91
|
+
if (isProcessRunning(pid)) {
|
|
92
|
+
try {
|
|
93
|
+
process.kill(pid, 'SIGKILL');
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Process might have exited between check and kill
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Clean up files
|
|
100
|
+
removeDaemonPid(projectRoot);
|
|
101
|
+
removeDaemonStatus(projectRoot);
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// Clean up files even on error
|
|
108
|
+
removeDaemonPid(projectRoot);
|
|
109
|
+
removeDaemonStatus(projectRoot);
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
error: `Failed to stop daemon: ${error.message}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function getDaemonStatus(projectRoot) {
|
|
117
|
+
const pid = readDaemonPid(projectRoot);
|
|
118
|
+
const status = readDaemonStatus(projectRoot);
|
|
119
|
+
if (!pid) {
|
|
120
|
+
return { running: false };
|
|
121
|
+
}
|
|
122
|
+
const running = isProcessRunning(pid);
|
|
123
|
+
if (!running) {
|
|
124
|
+
// Clean up stale files
|
|
125
|
+
removeDaemonPid(projectRoot);
|
|
126
|
+
removeDaemonStatus(projectRoot);
|
|
127
|
+
return { running: false };
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
running: true,
|
|
131
|
+
pid,
|
|
132
|
+
connected: status?.connected ?? false,
|
|
133
|
+
projectId: status?.project_id,
|
|
134
|
+
startedAt: status?.started_at,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Daemon process entry point.
|
|
4
|
+
* This script runs as a detached background process and manages the WebSocket connection.
|
|
5
|
+
* Matching extension's behavior.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node daemon.js <projectRoot> <projectId> <apiKey>
|
|
8
|
+
*/
|
|
9
|
+
import { WebSocketClient } from './websocket-client.js';
|
|
10
|
+
import { writeDaemonStatus, removeDaemonPid, removeDaemonStatus } from '../utils/config.js';
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
if (args.length < 3) {
|
|
13
|
+
console.error('Usage: daemon <projectRoot> <projectId> <apiKey>');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const [projectRoot, projectId, apiKey] = args;
|
|
17
|
+
let wsClient = null;
|
|
18
|
+
let isShuttingDown = false;
|
|
19
|
+
let startedAt = new Date().toISOString();
|
|
20
|
+
function updateStatus(connected) {
|
|
21
|
+
const status = {
|
|
22
|
+
pid: process.pid,
|
|
23
|
+
connected,
|
|
24
|
+
project_id: projectId,
|
|
25
|
+
started_at: startedAt,
|
|
26
|
+
};
|
|
27
|
+
writeDaemonStatus(status, projectRoot);
|
|
28
|
+
}
|
|
29
|
+
function cleanup() {
|
|
30
|
+
if (isShuttingDown)
|
|
31
|
+
return;
|
|
32
|
+
isShuttingDown = true;
|
|
33
|
+
console.log('[Daemon] Shutting down...');
|
|
34
|
+
if (wsClient) {
|
|
35
|
+
wsClient.disconnect();
|
|
36
|
+
wsClient = null;
|
|
37
|
+
}
|
|
38
|
+
removeDaemonPid(projectRoot);
|
|
39
|
+
removeDaemonStatus(projectRoot);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
// Handle graceful shutdown
|
|
43
|
+
process.on('SIGTERM', cleanup);
|
|
44
|
+
process.on('SIGINT', cleanup);
|
|
45
|
+
process.on('SIGHUP', cleanup);
|
|
46
|
+
// Handle uncaught errors
|
|
47
|
+
process.on('uncaughtException', (error) => {
|
|
48
|
+
console.error('[Daemon] Uncaught exception:', error);
|
|
49
|
+
cleanup();
|
|
50
|
+
});
|
|
51
|
+
process.on('unhandledRejection', (reason) => {
|
|
52
|
+
console.error('[Daemon] Unhandled rejection:', reason);
|
|
53
|
+
cleanup();
|
|
54
|
+
});
|
|
55
|
+
async function main() {
|
|
56
|
+
console.log('╔════════════════════════════════════════════╗');
|
|
57
|
+
console.log('║ Shift Daemon Starting ║');
|
|
58
|
+
console.log('╚════════════════════════════════════════════╝');
|
|
59
|
+
console.log(`[Daemon] Project ID: ${projectId}`);
|
|
60
|
+
console.log(`[Daemon] Project root: ${projectRoot}`);
|
|
61
|
+
console.log(`[Daemon] PID: ${process.pid}`);
|
|
62
|
+
// Initial status - not connected yet
|
|
63
|
+
updateStatus(false);
|
|
64
|
+
wsClient = new WebSocketClient({
|
|
65
|
+
projectId,
|
|
66
|
+
apiKey,
|
|
67
|
+
workspaceRoot: projectRoot,
|
|
68
|
+
});
|
|
69
|
+
// Set up event listeners - matching extension's setupWebSocketListeners
|
|
70
|
+
wsClient.on('connecting', () => {
|
|
71
|
+
console.log('[Daemon] WebSocket connecting...');
|
|
72
|
+
});
|
|
73
|
+
wsClient.on('connected', (projectInfo) => {
|
|
74
|
+
console.log('[Daemon] ✓ Connected to project:', projectInfo?.project_name);
|
|
75
|
+
updateStatus(true);
|
|
76
|
+
});
|
|
77
|
+
wsClient.on('disconnected', (code, reason) => {
|
|
78
|
+
console.log(`[Daemon] Disconnected (code: ${code}, reason: ${reason})`);
|
|
79
|
+
updateStatus(false);
|
|
80
|
+
});
|
|
81
|
+
wsClient.on('auth_failed', (message) => {
|
|
82
|
+
console.error(`[Daemon] ❌ Authentication failed: ${message}`);
|
|
83
|
+
updateStatus(false);
|
|
84
|
+
// Don't exit immediately, let the reconnect logic handle it
|
|
85
|
+
});
|
|
86
|
+
wsClient.on('error', (error) => {
|
|
87
|
+
console.error('[Daemon] WebSocket error:', error.message);
|
|
88
|
+
updateStatus(false);
|
|
89
|
+
});
|
|
90
|
+
wsClient.on('reconnecting', (attempt) => {
|
|
91
|
+
console.log(`[Daemon] Reconnecting... (attempt ${attempt})`);
|
|
92
|
+
});
|
|
93
|
+
wsClient.on('max_reconnects_reached', () => {
|
|
94
|
+
console.error('[Daemon] ❌ Max reconnection attempts reached. Stopping daemon.');
|
|
95
|
+
cleanup();
|
|
96
|
+
});
|
|
97
|
+
wsClient.on('message', (message) => {
|
|
98
|
+
console.log(`[Daemon] Received message: ${message.type}`);
|
|
99
|
+
});
|
|
100
|
+
wsClient.on('tool_executed', ({ tool, result, requestId }) => {
|
|
101
|
+
console.log(`[Daemon] ✓ Tool executed: ${tool} (${requestId})`);
|
|
102
|
+
});
|
|
103
|
+
wsClient.on('tool_error', ({ tool, error, requestId }) => {
|
|
104
|
+
console.error(`[Daemon] ✗ Tool failed: ${tool} (${requestId}):`, error.message);
|
|
105
|
+
});
|
|
106
|
+
// Connect to WebSocket
|
|
107
|
+
try {
|
|
108
|
+
await wsClient.connect();
|
|
109
|
+
console.log('[Daemon] ✓ Initial connection successful');
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error('[Daemon] Initial connection failed:', error.message);
|
|
113
|
+
// Don't exit - the WebSocket client will handle reconnection
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
main().catch((error) => {
|
|
117
|
+
console.error('[Daemon] Fatal error:', error);
|
|
118
|
+
cleanup();
|
|
119
|
+
});
|