@latentforce/shift 1.0.1 → 1.0.2

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
@@ -1,103 +1,108 @@
1
- # shift-mcp
1
+ # @latentforce/shift
2
2
 
3
- A minimal utility server built for the Model Context Protocol (MCP). It exposes tools that call the Shift Lite webapp backend for file summary, dependencies, and blast radius (using the project's knowledge graph).
3
+ A CLI and MCP server for AI-powered code intelligence. It provides:
4
4
 
5
- ## Environment variables
6
-
7
- - **`SHIFT_PROJECT_ID`** (optional if you pass per call): The default Shift Lite project UUID. If set, all tools use this project unless overridden by the `project_id` parameter in a tool call.
8
- - Example: `export SHIFT_PROJECT_ID=9af16a19-d073-4134-a0cd-272b0baf912e`
9
- - **`SHIFT_BACKEND_URL`** (optional): Webapp backend base URL. Default: `http://127.0.0.1:9000`.
10
-
11
- ## Project ID: env vs per-call
12
-
13
- - **Default:** Set `SHIFT_PROJECT_ID` in the MCP config (or env). All tools use that project.
14
- - **Override per call:** Each tool (`blast_radius`, `dependencies`, `file_summary`) accepts an optional **`project_id`** parameter. If you pass it, that call uses that project; otherwise the tool uses `SHIFT_PROJECT_ID`. So you can use one MCP for multiple projects by passing `project_id` in the call (e.g. "blast radius for project abc-123").
5
+ 1. **CLI commands** - Index and manage your project
6
+ 2. **MCP server** - Expose tools (blast_radius, dependencies, file_summary) to Claude Desktop/Code
15
7
 
16
8
  ## Installation
17
9
 
18
- ### Option 1: Install globally (simple, persistent)
19
-
20
10
  ```bash
21
11
  npm install -g @latentforce/shift
22
12
  ```
23
13
 
24
- Verify it works:
14
+ ## Quick Start
15
+
16
+ ### Step 1: Start and configure your project
17
+
25
18
  ```bash
26
- shift
19
+ cd /path/to/your/project
20
+ shift start
27
21
  ```
28
22
 
29
- You should see:
23
+ This will:
24
+ - Prompt for your Shift API key (saved to `~/.shift/config.json`)
25
+ - Let you select or enter your project ID
26
+ - Start a background daemon that connects to the backend
27
+
28
+ ### Step 2: Index your project
29
+
30
30
  ```bash
31
- Shift-lite MCP Server running on stdio
31
+ shift init
32
32
  ```
33
33
 
34
+ This scans your project files and sends the structure to the backend for indexing.
34
35
 
35
- ## Claude Code setup
36
+ ### Step 3: Configure MCP for Claude
36
37
 
37
- Claude Code does not auto-discover MCP servers. You must register it manually.
38
+ Now Claude can use the MCP tools. See [Claude Desktop setup](#claude-desktop-setup) or [Claude Code setup](#claude-code-setup) below.
38
39
 
39
- ### Install the Package (if not already)
40
- You can use this package without installing it globally by using npx, or install it globally for faster access.
40
+ ## CLI Commands
41
41
 
42
- ```bash
43
- npm install -g @latentforce/shift
44
- ```
42
+ | Command | Description |
43
+ |---------|-------------|
44
+ | `shift start` | Start the daemon and configure the project |
45
+ | `shift init` | Scan and index project files to backend |
46
+ | `shift stop` | Stop the running daemon |
47
+ | `shift status` | Show current status (API key, project, daemon, connection) |
48
+ | `shift` or `shift mcp` | Start MCP server on stdio (used by Claude) |
45
49
 
46
- ### Add to Claude Code
47
- - Replace SHIFT_BACKEND_URL and SHIFT_PROJECT_ID with correct values.
48
- - SHIFT_PROJECT_ID may be ignored as every tool accepts an optional **`project_id`** parameter.
49
- For Windows Users
50
- ```bash
51
- claude mcp add --scope user --transport stdio --env SHIFT_BACKEND_URL=BACKEND_URL --env SHIFT_PROJECT_ID=9af16a19-d073-4134-a0cd-272b0baf912e shift -- cmd /c npx -y @latentforce/shift
52
- ```
50
+ ## MCP Tools
53
51
 
54
- For macOS/Linux Users
55
- ```bash
56
- claude mcp add --scope user --transport stdio --env SHIFT_BACKEND_URL=BACKEND_URL --env SHIFT_PROJECT_ID=9af16a19-d073-4134-a0cd-272b0baf912e shift -- npx -y @latentforce/shift
57
- ```
52
+ The MCP server exposes these tools to Claude:
58
53
 
59
- ### Verify Installation
60
- ```bash
61
- claude mcp list
62
- ```
54
+ | Tool | Description |
55
+ |------|-------------|
56
+ | `blast_radius` | Analyze what would be affected if a file is modified |
57
+ | `dependencies` | Get all dependencies for a file (direct and transitive) |
58
+ | `file_summary` | Generate a summary of a file with optional parent context |
63
59
 
64
- ### Use It
65
- Start a Claude Code session:
66
- ```bash
67
- claude
68
- ```
60
+ ## Environment Variables
69
61
 
70
- ## Claude Desktop setup
62
+ | Variable | Description | Default |
63
+ |----------|-------------|---------|
64
+ | `SHIFT_PROJECT_ID` | Default project UUID (can be overridden per tool call) | - |
65
+ | `SHIFT_BACKEND_URL` | Backend API URL | `http://127.0.0.1:9000` |
66
+ | `SHIFT_API_URL` | API URL for CLI | `http://localhost:9000` |
67
+ | `SHIFT_ORCH_URL` | Orchestrator URL | `http://localhost:9999` |
68
+ | `SHIFT_WS_URL` | WebSocket URL | `ws://localhost:9999` |
71
69
 
72
- Claude Desktop does not auto-discover MCP servers. You must register it manually.
70
+ ## Claude Code Setup
73
71
 
74
- ### Config file location
72
+ ### Add MCP Server
75
73
 
76
- **macOS (Claude Desktop app):**
77
74
  ```bash
78
- ~/Library/Application Support/Claude/claude_desktop_config.json
75
+ claude mcp add-json shift '{"type":"stdio","command":"shift","args":[],"env":{"SHIFT_PROJECT_ID":"YOUR_PROJECT_ID_HERE","SHIFT_BACKEND_URL":"https://dev-shift-lite.latentforce.ai"}}'
79
76
  ```
80
- *(In Claude Desktop: Claude menu → Settings… → Developer → Edit Config)*
81
77
 
82
- **Linux (if different):**
83
- ```bash
84
- ~/.config/claude-desktop/claude_desktop_config.json
85
- ```
78
+ ### Verify
86
79
 
87
- **Windows:**
88
80
  ```bash
89
- %APPDATA%\Claude\claude_desktop_config.json
81
+ claude mcp list
90
82
  ```
91
83
 
92
- Add this (replace `YOUR_PROJECT_UUID` with your Shift Lite project ID):
84
+ ## Claude Desktop Setup
85
+
86
+ ### Config File Location
87
+
88
+ | OS | Path |
89
+ |----|------|
90
+ | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
91
+ | Linux | `~/.config/claude-desktop/claude_desktop_config.json` |
92
+ | Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
93
+
94
+ ### Configuration
95
+
96
+ Add to your config file:
97
+
93
98
  ```json
94
99
  {
95
100
  "mcpServers": {
96
101
  "shift": {
97
102
  "command": "shift",
98
103
  "env": {
99
- "SHIFT_BACKEND_URL": "BACKEND URL",
100
- "SHIFT_PROJECT_ID": "YOUR_PROJECT_UUID"
104
+ "SHIFT_BACKEND_URL": "https://dev-shift-lite.latentforce.ai",
105
+ "SHIFT_PROJECT_ID": "YOUR_PROJECT_ID_HERE"
101
106
  }
102
107
  }
103
108
  }
@@ -106,16 +111,55 @@ Add this (replace `YOUR_PROJECT_UUID` with your Shift Lite project ID):
106
111
 
107
112
  Restart Claude Desktop after saving.
108
113
 
114
+ ## Project ID
115
+
116
+ You can set the project ID in two ways:
117
+
118
+ 1. **Environment variable**: Set `SHIFT_PROJECT_ID` in MCP config
119
+ 2. **Per-call override**: Pass `project_id` parameter in each tool call
120
+
121
+ This allows one MCP server to work with multiple projects.
122
+
109
123
  ## Development
110
124
 
111
- Clone and build:
112
125
  ```bash
126
+ # Clone and install
113
127
  npm install
128
+
129
+ # Build
114
130
  npm run build
115
- ```
116
131
 
117
- Run locally:
118
- ```bash
132
+ # Run locally
119
133
  node build/index.js
134
+
135
+ # Test CLI commands
136
+ node build/index.js start
137
+ node build/index.js init
138
+ node build/index.js status
139
+ node build/index.js stop
120
140
  ```
121
141
 
142
+ ## How It All Works Together
143
+
144
+ ```
145
+ ┌─────────────────────────────────────────────────────────────────┐
146
+ │ User's Machine │
147
+ │ │
148
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
149
+ │ │ shift start │────▶│ Daemon │────▶│ Backend (WS) │ │
150
+ │ │ shift init │ │ (background)│ │ │ │
151
+ │ │ shift stop │ └─────────────┘ │ │ │
152
+ │ │ shift status│ │ │ │
153
+ │ └─────────────┘ │ Shift Lite │ │
154
+ │ │ Backend │ │
155
+ │ ┌─────────────┐ ┌─────────────┐ │ │ │
156
+ │ │ Claude │────▶│ shift (MCP) │────▶│ (REST API) │ │
157
+ │ │ Desktop/Code│ │ Server │ │ │ │
158
+ │ └─────────────┘ └─────────────┘ └─────────────────┘ │
159
+ │ │
160
+ └─────────────────────────────────────────────────────────────────┘
161
+
162
+ 1. `shift start` - Starts daemon, connects to backend via WebSocket
163
+ 2. `shift init` - Indexes project files to backend
164
+ 3. Claude calls MCP tools - MCP server queries backend REST API
165
+ ```
@@ -0,0 +1,136 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { getApiKey, setApiKey, readProjectConfig, writeProjectConfig } from '../../utils/config.js';
4
+ import { promptApiKey } from '../../utils/prompts.js';
5
+ import { getDaemonStatus } from '../../daemon/daemon-manager.js';
6
+ import { getProjectTree, extractAllFilePaths, categorizeFiles } from '../../utils/tree-scanner.js';
7
+ import { sendInitScan } from '../../utils/api-client.js';
8
+ const execAsync = promisify(exec);
9
+ export async function initCommand() {
10
+ const projectRoot = process.cwd();
11
+ console.log('╔═══════════════════════════════════════════════╗');
12
+ console.log('║ Initializing Shift Project ║');
13
+ console.log('╚═══════════════════════════════════════════════╝\n');
14
+ // Step 1: Check API key
15
+ console.log('[Init] Step 1/5: Checking API key...');
16
+ let apiKey = getApiKey();
17
+ if (!apiKey) {
18
+ apiKey = await promptApiKey();
19
+ setApiKey(apiKey);
20
+ console.log('[Init] ✓ API key saved\n');
21
+ }
22
+ else {
23
+ console.log('[Init] ✓ API key found\n');
24
+ }
25
+ // Step 2: Check project config
26
+ console.log('[Init] Step 2/5: Checking project configuration...');
27
+ const projectConfig = readProjectConfig(projectRoot);
28
+ if (!projectConfig) {
29
+ console.error('\n❌ No project configured for this directory.');
30
+ console.log('Run "shift start" first to configure the project.\n');
31
+ process.exit(1);
32
+ }
33
+ console.log(`[Init] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
34
+ // Step 3: Check daemon status (warn if not running)
35
+ console.log('[Init] Step 3/5: Checking daemon status...');
36
+ const status = getDaemonStatus(projectRoot);
37
+ if (!status.running) {
38
+ console.log('[Init] ⚠️ Warning: Daemon is not running. Run "shift start" to start it.\n');
39
+ }
40
+ else {
41
+ console.log(`[Init] ✓ Daemon running (PID: ${status.pid}, Connected: ${status.connected})\n`);
42
+ }
43
+ // Step 4: Scan project structure (matching extension's Step 6)
44
+ console.log('[Init] Step 4/5: Scanning project structure...');
45
+ // Get project tree (matching extension's getProjectTree)
46
+ const treeData = getProjectTree(projectRoot, {
47
+ depth: 0, // Unlimited depth
48
+ exclude_patterns: [
49
+ '.git',
50
+ 'node_modules',
51
+ '__pycache__',
52
+ '.vscode',
53
+ 'dist',
54
+ 'build',
55
+ '.shift',
56
+ '.next',
57
+ 'coverage',
58
+ 'venv',
59
+ 'env',
60
+ ],
61
+ });
62
+ console.log(`[Init] Files: ${treeData.file_count}`);
63
+ console.log(`[Init] Directories: ${treeData.dir_count}`);
64
+ console.log(`[Init] Total size: ${treeData.total_size_mb} MB\n`);
65
+ // Get git info (matching extension)
66
+ let gitInfo = {
67
+ current_branch: '',
68
+ original_branch: 'shift_original',
69
+ migrate_branch: 'shift_migrated',
70
+ has_uncommitted_changes: false,
71
+ };
72
+ try {
73
+ const { stdout: currentBranch } = await execAsync('git branch --show-current', { cwd: projectRoot });
74
+ const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: projectRoot });
75
+ gitInfo.current_branch = currentBranch.trim();
76
+ gitInfo.has_uncommitted_changes = statusOutput.trim().length > 0;
77
+ console.log(`[Init] ✓ Git info: branch=${gitInfo.current_branch}, uncommitted=${gitInfo.has_uncommitted_changes}\n`);
78
+ }
79
+ catch {
80
+ console.log('[Init] ⚠️ Not a git repository or git not available\n');
81
+ }
82
+ // Extract file paths and categorize (matching extension)
83
+ const allFiles = extractAllFilePaths(treeData.tree);
84
+ const categorized = categorizeFiles(treeData.tree);
85
+ console.log(`[Init] Source files: ${categorized.source_files.length}`);
86
+ console.log(`[Init] Config files: ${categorized.config_files.length}`);
87
+ console.log(`[Init] Asset files: ${categorized.asset_files.length}\n`);
88
+ // Step 5: Send scan to backend (matching extension's Step 9)
89
+ console.log('[Init] Step 5/5: Sending scan to backend...');
90
+ const payload = {
91
+ project_id: projectConfig.project_id,
92
+ project_tree: treeData,
93
+ git_info: gitInfo,
94
+ file_manifest: {
95
+ all_files: allFiles,
96
+ categorized: categorized,
97
+ },
98
+ metadata: {
99
+ extension_version: '1.0.2-cli', // CLI version
100
+ scan_timestamp: new Date().toISOString(),
101
+ project_name: projectConfig.project_name,
102
+ },
103
+ };
104
+ try {
105
+ const response = await sendInitScan(apiKey, projectConfig.project_id, payload);
106
+ console.log('[Init] ✓ Backend initialization completed');
107
+ console.log(`[Init] Files read: ${response.files_read}`);
108
+ console.log(`[Init] Files failed: ${response.files_failed}`);
109
+ // Update local config with agent info (matching extension)
110
+ if (response.agents_created?.theme_planner) {
111
+ const agentInfo = {
112
+ agent_id: response.agents_created.theme_planner.agent_id,
113
+ agent_name: response.agents_created.theme_planner.agent_name,
114
+ agent_type: 'theme_planner',
115
+ created_at: new Date().toISOString(),
116
+ };
117
+ projectConfig.agents = projectConfig.agents || [];
118
+ projectConfig.agents.push(agentInfo);
119
+ writeProjectConfig(projectConfig, projectRoot);
120
+ console.log(`[Init] ✓ Agent info saved: ${agentInfo.agent_id}`);
121
+ }
122
+ console.log('\n╔═══════════════════════════════════════════════╗');
123
+ console.log('║ ✓ Project Initialization Complete ║');
124
+ console.log('╚═══════════════════════════════════════════════╝');
125
+ if (response.next_steps) {
126
+ console.log('\nNext steps:');
127
+ response.next_steps.forEach((step) => console.log(` - ${step}`));
128
+ }
129
+ console.log('\nFile indexing has been triggered. This may take a few moments.');
130
+ console.log('Use "shift status" to check the daemon connection.\n');
131
+ }
132
+ catch (error) {
133
+ console.error(`\n❌ Failed to initialize project: ${error.message}`);
134
+ process.exit(1);
135
+ }
136
+ }
@@ -0,0 +1,81 @@
1
+ import { getApiKey, setApiKey, setProject, readProjectConfig } from '../../utils/config.js';
2
+ import { promptApiKey, promptProjectId, promptSelectProject } from '../../utils/prompts.js';
3
+ import { startDaemon, getDaemonStatus } from '../../daemon/daemon-manager.js';
4
+ import { fetchProjects } from '../../utils/api-client.js';
5
+ export async function startCommand() {
6
+ const projectRoot = process.cwd();
7
+ console.log('╔════════════════════════════════════════════╗');
8
+ console.log('║ Starting Shift ║');
9
+ console.log('╚════════════════════════════════════════════╝\n');
10
+ // Step 1: Ensure API key exists
11
+ console.log('[Start] Step 1/4: Checking API key...');
12
+ let apiKey = getApiKey();
13
+ if (!apiKey) {
14
+ apiKey = await promptApiKey();
15
+ setApiKey(apiKey);
16
+ console.log('[Start] ✓ API key saved to ~/.shift/config.json\n');
17
+ }
18
+ else {
19
+ console.log('[Start] ✓ API key found\n');
20
+ }
21
+ // Step 2: Check/create project config
22
+ console.log('[Start] Step 2/4: Checking project configuration...');
23
+ let projectConfig = readProjectConfig(projectRoot);
24
+ if (!projectConfig) {
25
+ // Fetch available projects
26
+ console.log('[Start] Fetching your projects...');
27
+ try {
28
+ const projects = await fetchProjects(apiKey);
29
+ if (!projects || projects.length === 0) {
30
+ console.error('\n❌ No projects found. Create one in the Shift dashboard first.');
31
+ process.exit(1);
32
+ }
33
+ console.log(`[Start] Found ${projects.length} project(s)\n`);
34
+ // Let user select a project
35
+ const selected = await promptSelectProject(projects);
36
+ // Save project config (matching extension's .shift/config.json structure)
37
+ setProject(selected.project_id, selected.project_name, projectRoot);
38
+ console.log(`[Start] ✓ Project "${selected.project_name}" saved to .shift/config.json\n`);
39
+ projectConfig = readProjectConfig(projectRoot);
40
+ }
41
+ catch (error) {
42
+ // If fetching fails, fall back to manual entry
43
+ console.error(`\n⚠️ Could not fetch projects: ${error.message}`);
44
+ console.log('Falling back to manual project ID entry...\n');
45
+ const projectId = await promptProjectId();
46
+ setProject(projectId, 'Unknown Project', projectRoot);
47
+ console.log(`[Start] ✓ Project ID saved to .shift/config.json\n`);
48
+ projectConfig = readProjectConfig(projectRoot);
49
+ }
50
+ }
51
+ else {
52
+ console.log(`[Start] ✓ Project found: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
53
+ }
54
+ if (!projectConfig) {
55
+ console.error('❌ Failed to configure project');
56
+ process.exit(1);
57
+ }
58
+ // Step 3: Check if daemon is already running
59
+ console.log('[Start] Step 3/4: Checking daemon status...');
60
+ const status = getDaemonStatus(projectRoot);
61
+ if (status.running) {
62
+ console.log(`[Start] ✓ Daemon is already running (PID: ${status.pid})`);
63
+ console.log(`[Start] WebSocket: ${status.connected ? 'Connected' : 'Connecting...'}\n`);
64
+ return;
65
+ }
66
+ console.log('[Start] Daemon not running\n');
67
+ // Step 4: Start daemon
68
+ console.log('[Start] Step 4/4: Starting daemon...');
69
+ const result = await startDaemon(projectRoot, projectConfig.project_id, apiKey);
70
+ if (!result.success) {
71
+ console.error(`\n❌ Failed to start daemon: ${result.error}`);
72
+ process.exit(1);
73
+ }
74
+ console.log(`[Start] ✓ Daemon started (PID: ${result.pid})`);
75
+ console.log('\n╔════════════════════════════════════════════╗');
76
+ console.log('║ Shift is now running! ║');
77
+ console.log('╚════════════════════════════════════════════╝');
78
+ console.log('\nUse "shift status" to check connection status.');
79
+ console.log('Use "shift init" to scan and index the project.');
80
+ console.log('Use "shift stop" to stop the daemon.\n');
81
+ }
@@ -0,0 +1,46 @@
1
+ import { getApiKey, readProjectConfig } 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
+ console.log('API Key: ✓ Configured');
16
+ // Check project config
17
+ const projectConfig = readProjectConfig(projectRoot);
18
+ if (!projectConfig) {
19
+ console.log('Project: ❌ Not configured');
20
+ console.log('\nRun "shift start" to configure this project.\n');
21
+ return;
22
+ }
23
+ console.log(`Project: ${projectConfig.project_name}`);
24
+ console.log(`Project ID: ${projectConfig.project_id}`);
25
+ // Check agents
26
+ if (projectConfig.agents && projectConfig.agents.length > 0) {
27
+ console.log(`Agents: ${projectConfig.agents.length} configured`);
28
+ projectConfig.agents.forEach((agent) => {
29
+ console.log(` - ${agent.agent_name} (${agent.agent_type})`);
30
+ });
31
+ }
32
+ // Check daemon status
33
+ const status = getDaemonStatus(projectRoot);
34
+ if (!status.running) {
35
+ console.log('Daemon: ❌ Not running');
36
+ console.log('\nRun "shift start" to start the daemon.\n');
37
+ return;
38
+ }
39
+ console.log(`Daemon: ✓ Running (PID: ${status.pid})`);
40
+ console.log(`WebSocket: ${status.connected ? '✓ Connected' : '⚠️ Disconnected'}`);
41
+ if (status.startedAt) {
42
+ const startedAt = new Date(status.startedAt);
43
+ console.log(`Started: ${startedAt.toLocaleString()}`);
44
+ }
45
+ console.log('');
46
+ }
@@ -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
+ }