@latentforce/shift 1.0.6 → 1.0.7

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.
@@ -1,13 +1,29 @@
1
1
  import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
+ import { createInterface } from 'readline';
3
4
  import { getApiKey, setApiKey, setGuestKey, isGuestKey, readProjectConfig, writeProjectConfig } from '../../utils/config.js';
4
5
  import { promptApiKey, promptKeyChoice } from '../../utils/prompts.js';
5
- import { requestGuestKey } from '../../utils/api-client.js';
6
+ import { requestGuestKey, fetchProjectStatus } from '../../utils/api-client.js';
6
7
  import { getDaemonStatus } from '../../daemon/daemon-manager.js';
7
8
  import { getProjectTree, extractAllFilePaths, categorizeFiles } from '../../utils/tree-scanner.js';
8
9
  import { sendInitScan } from '../../utils/api-client.js';
9
10
  const execAsync = promisify(exec);
10
- export async function initCommand() {
11
+ /**
12
+ * Prompt user to confirm re-indexing an already indexed project
13
+ */
14
+ async function promptForReindex() {
15
+ const rl = createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout,
18
+ });
19
+ return new Promise((resolve) => {
20
+ rl.question('Do you want to re-index the project? (y/N): ', (answer) => {
21
+ rl.close();
22
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
23
+ });
24
+ });
25
+ }
26
+ export async function initCommand(options = {}) {
11
27
  const projectRoot = process.cwd();
12
28
  console.log('╔═══════════════════════════════════════════════╗');
13
29
  console.log('║ Initializing Shift Project ║');
@@ -51,6 +67,32 @@ export async function initCommand() {
51
67
  process.exit(1);
52
68
  }
53
69
  console.log(`[Init] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
70
+ // Check if project is already indexed (skip check if --force flag is used)
71
+ if (!options.force) {
72
+ try {
73
+ const projectStatus = await fetchProjectStatus(apiKey, projectConfig.project_id);
74
+ if (projectStatus.indexed) {
75
+ console.log(`[Init] ✓ Project already indexed (${projectStatus.file_count} files)\n`);
76
+ const shouldReindex = await promptForReindex();
77
+ if (!shouldReindex) {
78
+ console.log('\n╔═══════════════════════════════════════════════╗');
79
+ console.log('║ Project Already Initialized ║');
80
+ console.log('╚═══════════════════════════════════════════════╝');
81
+ console.log('\nUse "shift status" to check the current status.');
82
+ console.log('Use "shift init --force" to force re-indexing.\n');
83
+ return;
84
+ }
85
+ console.log('\n[Init] Proceeding with re-indexing...\n');
86
+ }
87
+ }
88
+ catch {
89
+ // If we can't check status, continue with init (server might be unavailable)
90
+ console.log('[Init] ⚠️ Could not check indexing status, proceeding with initialization...\n');
91
+ }
92
+ }
93
+ else {
94
+ console.log('[Init] Force flag detected, skipping indexing status check...\n');
95
+ }
54
96
  // Step 3: Check daemon status (warn if not running)
55
97
  console.log('[Init] Step 3/5: Checking daemon status...');
56
98
  const status = getDaemonStatus(projectRoot);
@@ -1,5 +1,6 @@
1
1
  import { getApiKey, readProjectConfig, isGuestKey } from '../../utils/config.js';
2
2
  import { getDaemonStatus } from '../../daemon/daemon-manager.js';
3
+ import { fetchProjectStatus } from '../../utils/api-client.js';
3
4
  export async function statusCommand() {
4
5
  const projectRoot = process.cwd();
5
6
  console.log('\n╔════════════════════════════════════════════╗');
@@ -34,6 +35,20 @@ export async function statusCommand() {
34
35
  console.log(` - ${agent.agent_name} (${agent.agent_type})`);
35
36
  });
36
37
  }
38
+ // Check project indexing status (work done)
39
+ try {
40
+ const projectStatus = await fetchProjectStatus(apiKey, projectConfig.project_id);
41
+ if (projectStatus.indexed) {
42
+ console.log(`Indexed: ✓ Complete (${projectStatus.file_count} files)`);
43
+ }
44
+ else {
45
+ console.log('Indexed: ❌ Not indexed');
46
+ console.log(' Run "shift-cli init" to scan the project.');
47
+ }
48
+ }
49
+ catch {
50
+ console.log('Indexed: ⚠️ Unable to check (server unavailable)');
51
+ }
37
52
  // Check daemon status
38
53
  const status = getDaemonStatus(projectRoot);
39
54
  if (!status.running) {
package/build/index.js CHANGED
@@ -31,9 +31,10 @@ program
31
31
  program
32
32
  .command('init')
33
33
  .description('Initialize and scan the project for file indexing')
34
- .action(async () => {
34
+ .option('-f, --force', 'Force re-indexing even if project is already indexed')
35
+ .action(async (options) => {
35
36
  const { initCommand } = await import('./cli/commands/init.js');
36
- await initCommand();
37
+ await initCommand({ force: options.force ?? false });
37
38
  });
38
39
  program
39
40
  .command('stop')
@@ -1,13 +1,23 @@
1
1
  import { API_BASE_URL, API_BASE_URL_ORCH } from './config.js';
2
+ import { getMachineFingerprint } from './machine-id.js';
2
3
  /**
3
- * Request a guest API key
4
+ * Request a guest API key bound to this machine
5
+ * The backend will:
6
+ * - Create a new guest key if this machine_id hasn't been seen before
7
+ * - Return the existing guest key if this machine_id already has one
8
+ * - Optionally enforce usage limits per machine
4
9
  */
5
10
  export async function requestGuestKey() {
11
+ const fingerprint = getMachineFingerprint();
6
12
  const response = await fetch(`${API_BASE_URL}/api/guest-key`, {
7
13
  method: 'POST',
8
14
  headers: {
9
15
  'Content-Type': 'application/json',
10
16
  },
17
+ body: JSON.stringify({
18
+ machine_id: fingerprint.machine_id,
19
+ platform: fingerprint.platform,
20
+ }),
11
21
  });
12
22
  if (!response.ok) {
13
23
  const text = await response.text();
@@ -64,3 +74,30 @@ export async function sendInitScan(apiKey, projectId, payload) {
64
74
  throw error;
65
75
  }
66
76
  }
77
+ /**
78
+ * Fetch project indexing/work status
79
+ * Returns whether the knowledge graph has been built for this project
80
+ */
81
+ export async function fetchProjectStatus(apiKey, projectId) {
82
+ const controller = new AbortController();
83
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
84
+ try {
85
+ const response = await fetch(`${API_BASE_URL}/api/projects/${projectId}/status`, {
86
+ method: 'GET',
87
+ headers: {
88
+ 'Authorization': `Bearer ${apiKey}`,
89
+ },
90
+ signal: controller.signal,
91
+ });
92
+ clearTimeout(timeoutId);
93
+ if (!response.ok) {
94
+ const text = await response.text();
95
+ throw new Error(text || `HTTP ${response.status}`);
96
+ }
97
+ return await response.json();
98
+ }
99
+ catch (error) {
100
+ clearTimeout(timeoutId);
101
+ throw error;
102
+ }
103
+ }
@@ -10,9 +10,9 @@ const LOCAL_CONFIG_FILE = 'config.json'; // Changed from project.json to match e
10
10
  const DAEMON_PID_FILE = 'daemon.pid';
11
11
  const DAEMON_STATUS_FILE = 'daemon.status.json';
12
12
  // Default URLs
13
- const DEFAULT_API_URL = 'https://dev-shift-lite.latentforce.ai';
14
- const DEFAULT_ORCH_URL = 'https://agent-orch.latentforce.ai';
15
- const DEFAULT_WS_URL = 'wss://agent-orch.latentforce.ai';
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
16
  // Helper to get URL from: 1) env var, 2) global config, 3) default
17
17
  function getConfiguredUrl(envVar, configKey, defaultUrl) {
18
18
  // Priority: Environment variable > Global config > Default
@@ -0,0 +1,46 @@
1
+ import os from 'os';
2
+ import crypto from 'crypto';
3
+ /**
4
+ * Get the primary MAC address (first non-internal interface)
5
+ */
6
+ function getPrimaryMacAddress() {
7
+ const interfaces = os.networkInterfaces();
8
+ for (const [name, addrs] of Object.entries(interfaces)) {
9
+ if (!addrs)
10
+ continue;
11
+ for (const addr of addrs) {
12
+ // Skip internal/loopback and addresses without MAC
13
+ if (addr.internal || !addr.mac || addr.mac === '00:00:00:00:00:00') {
14
+ continue;
15
+ }
16
+ return addr.mac;
17
+ }
18
+ }
19
+ return 'unknown';
20
+ }
21
+ /**
22
+ * Generate a unique, deterministic machine fingerprint
23
+ * Uses MAC address + platform for stability
24
+ * - MAC is globally unique per network interface
25
+ * - Platform helps distinguish dual-boot scenarios
26
+ */
27
+ export function generateMachineId() {
28
+ const mac = getPrimaryMacAddress();
29
+ const platform = os.platform();
30
+ // Combine MAC + platform
31
+ const fingerprintData = `${mac}|${platform}`;
32
+ // Hash for privacy and fixed length
33
+ return crypto.createHash('sha256').update(fingerprintData).digest('hex');
34
+ }
35
+ /**
36
+ * Generate a shorter machine ID (first 16 chars of full hash)
37
+ */
38
+ export function generateShortMachineId() {
39
+ return generateMachineId().substring(0, 16);
40
+ }
41
+ export function getMachineFingerprint() {
42
+ return {
43
+ machine_id: generateMachineId(),
44
+ platform: os.platform(),
45
+ };
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latentforce/shift",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Shift CLI - AI-powered code intelligence with MCP support",
5
5
  "type": "module",
6
6
  "main": "./build/index.js",