@latentforce/shift 1.0.15 → 1.0.16

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.
@@ -249,7 +249,13 @@ export async function initCommand(options = {}) {
249
249
  try {
250
250
  const response = await sendInitScan(apiKey, project.projectId, payload);
251
251
  console.log('[Init] ✓ Backend initialization completed');
252
- console.log(`[Init] Source files queued: ${response.source_files_count}`);
252
+ const count = response.source_files_count ?? response.files_read;
253
+ if (count != null) {
254
+ console.log(`[Init] Source files queued: ${count}`);
255
+ }
256
+ else {
257
+ console.log(`[Init] (backend did not return file count)`);
258
+ }
253
259
  // Update local config with agent info (matching extension)
254
260
  if (response.agents_created?.theme_planner) {
255
261
  const agentInfo = {
@@ -22,6 +22,9 @@ export class ToolsExecutor {
22
22
  'get_repository_root': this.getRepositoryRoot.bind(this),
23
23
  'get_project_tree': this.getProjectTree.bind(this),
24
24
  'execute_command': this.executeCommand.bind(this),
25
+ 'run_bash': this.runBash.bind(this),
26
+ 'glob_search': this.globSearch.bind(this),
27
+ 'grep_search': this.grepSearch.bind(this),
25
28
  };
26
29
  }
27
30
  /**
@@ -285,6 +288,149 @@ export class ToolsExecutor {
285
288
  message: `No .git folder found after searching ${maxDepth} levels up`
286
289
  };
287
290
  }
291
+ /**
292
+ * Returns combined output matching tools.py's run_bash format.
293
+ */
294
+ async runBash(params) {
295
+ const { command } = params;
296
+ if (!command) {
297
+ return { status: 'error', error: 'command is required' };
298
+ }
299
+ console.log(`[ToolsExecutor] run_bash: ${command}`);
300
+ try {
301
+ const { stdout, stderr } = await execAsync(command, {
302
+ cwd: this.workspaceRoot,
303
+ timeout: 30000,
304
+ maxBuffer: 1024 * 1024,
305
+ });
306
+ let output = stdout;
307
+ if (stderr)
308
+ output += `\n[STDERR]\n${stderr}`;
309
+ return {
310
+ status: 'success',
311
+ output: output.trim() || '[No output]',
312
+ };
313
+ }
314
+ catch (error) {
315
+ let output = error.stdout || '';
316
+ if (error.stderr)
317
+ output += `\n[STDERR]\n${error.stderr}`;
318
+ output += `\n[EXIT CODE: ${error.code || 1}]`;
319
+ return {
320
+ status: 'error',
321
+ output: output.trim(),
322
+ error: error.message,
323
+ };
324
+ }
325
+ }
326
+ /**
327
+ * Find files matching a glob pattern
328
+ */
329
+ async globSearch(params) {
330
+ const { pattern } = params;
331
+ if (!pattern) {
332
+ return { status: 'error', error: 'pattern is required' };
333
+ }
334
+ console.log(`[ToolsExecutor] Glob search: ${pattern}`);
335
+ let basePath = '.';
336
+ let namePattern = pattern;
337
+ const lastSlash = pattern.lastIndexOf('/');
338
+ if (lastSlash !== -1) {
339
+ basePath = pattern.substring(0, lastSlash).replace(/\/?\*\*$/, '') || '.';
340
+ namePattern = pattern.substring(lastSlash + 1);
341
+ }
342
+ const searchDir = path.join(this.workspaceRoot, basePath);
343
+ const excludes = [
344
+ '-not', '-path', '*/node_modules/*',
345
+ '-not', '-path', '*/.git/*',
346
+ '-not', '-path', '*/__pycache__/*',
347
+ '-not', '-path', '*/dist/*',
348
+ '-not', '-path', '*/build/*',
349
+ '-not', '-path', '*/.venv/*',
350
+ ];
351
+ // Quote searchDir to handle workspace paths that contain spaces
352
+ const cmd = `find "${searchDir}" -name "${namePattern}" ${excludes.join(' ')}`;
353
+ try {
354
+ const { stdout } = await execAsync(cmd, { timeout: 30000 });
355
+ const matches = stdout
356
+ .trim()
357
+ .split('\n')
358
+ .filter(Boolean)
359
+ .map(f => path.relative(this.workspaceRoot, f))
360
+ .slice(0, 200);
361
+ return {
362
+ status: 'success',
363
+ pattern,
364
+ matches,
365
+ count: matches.length,
366
+ };
367
+ }
368
+ catch (error) {
369
+ return { status: 'error', error: error.message };
370
+ }
371
+ }
372
+ /**
373
+ * Search file contents with a regex pattern
374
+ */
375
+ async grepSearch(params) {
376
+ const { pattern, file_glob } = params;
377
+ if (!pattern) {
378
+ return { status: 'error', error: 'pattern is required' };
379
+ }
380
+ console.log(`[ToolsExecutor] Grep search: ${pattern}${file_glob ? ` (${file_glob})` : ''}`);
381
+ const baseArgs = [
382
+ 'grep', '-rn', '-E',
383
+ '--exclude-dir=node_modules',
384
+ '--exclude-dir=.git',
385
+ '--exclude-dir=__pycache__',
386
+ '--exclude-dir=dist',
387
+ '--exclude-dir=build',
388
+ '--exclude-dir=.venv',
389
+ '--binary-files=without-match',
390
+ ];
391
+ let searchPath = '.';
392
+ if (file_glob) {
393
+ if (file_glob.includes('/')) {
394
+ const match = file_glob.match(/^([^*]+?)\/(?:\*\*\/)?(.+)$/);
395
+ if (match) {
396
+ searchPath = match[1];
397
+ const namePat = match[2];
398
+ if (namePat && namePat !== '*')
399
+ baseArgs.push(`--include=${namePat}`);
400
+ }
401
+ else {
402
+ baseArgs.push(`--include=${file_glob}`);
403
+ }
404
+ }
405
+ else {
406
+ baseArgs.push(`--include=${file_glob}`);
407
+ }
408
+ }
409
+ const args = [...baseArgs, `'${pattern.replace(/'/g, `'\\''`)}'`, searchPath];
410
+ try {
411
+ const { stdout } = await execAsync(args.join(' '), {
412
+ cwd: this.workspaceRoot,
413
+ timeout: 30000,
414
+ maxBuffer: 1024 * 1024,
415
+ });
416
+ const lines = stdout.trim().split('\n').filter(Boolean);
417
+ const truncated = lines.length > 500;
418
+ const output = truncated ? lines.slice(0, 500).join('\n') + `\n\n... [truncated — ${lines.length} total matches, showing first 500]` : stdout.trim();
419
+ return {
420
+ status: 'success',
421
+ pattern,
422
+ file_glob: file_glob || null,
423
+ output,
424
+ match_count: lines.length,
425
+ };
426
+ }
427
+ catch (error) {
428
+ if (error.code === 1 && !error.stderr) {
429
+ return { status: 'success', pattern, file_glob: file_glob || null, output: '', match_count: 0 };
430
+ }
431
+ return { status: 'error', error: error.message };
432
+ }
433
+ }
288
434
  /**
289
435
  * Get project tree — delegates to shared tree-scanner utility
290
436
  */
@@ -110,6 +110,24 @@ function formatDependencies(data) {
110
110
  }
111
111
  }
112
112
  }
113
+ const implicitDeps = data.implicit_dependencies || [];
114
+ if (implicitDeps.length > 0) {
115
+ lines.push('');
116
+ lines.push('## Implicit / external dependencies');
117
+ lines.push('');
118
+ lines.push(`Implicit dependencies (**${implicitDeps.length}**):`);
119
+ lines.push('');
120
+ const implicitEdges = data.implicit_edge_summaries || {};
121
+ for (const dep of implicitDeps) {
122
+ const summary = implicitEdges[dep];
123
+ if (summary) {
124
+ lines.push(`- **${dep}**: ${summary}`);
125
+ }
126
+ else {
127
+ lines.push(`- **${dep}**`);
128
+ }
129
+ }
130
+ }
113
131
  return lines.join('\n');
114
132
  }
115
133
  /** Format blast_radius response as readable markdown */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latentforce/shift",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "Shift CLI - AI-powered code intelligence with MCP support",
5
5
  "type": "module",
6
6
  "main": "./build/index.js",