@gabrihhh/jarvis 2.0.3 → 2.1.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/bin/jarvis.js CHANGED
@@ -7,6 +7,27 @@ import { homedir } from 'os';
7
7
  import { exec } from 'child_process';
8
8
  import { fileURLToPath } from 'url';
9
9
 
10
+ const MEMORY_CONFIG_PATH = join(homedir(), '.claude-memory.json');
11
+
12
+ function loadMemoryConfig() {
13
+ try { return JSON.parse(readFileSync(MEMORY_CONFIG_PATH, 'utf-8')); }
14
+ catch { return { neo4j: { uri: 'bolt://localhost:7687', user: 'neo4j', password: 'claudememory' } }; }
15
+ }
16
+
17
+ function saveMemoryConfig(cfg) {
18
+ writeFileSync(MEMORY_CONFIG_PATH, JSON.stringify(cfg, null, 2));
19
+ }
20
+
21
+ function setHook(settings, enabled) {
22
+ if (!settings.hooks) settings.hooks = {};
23
+ if (enabled) {
24
+ settings.hooks.UserPromptSubmit = [{ type: 'command', command: 'jarvis --query' }];
25
+ } else {
26
+ delete settings.hooks.UserPromptSubmit;
27
+ if (!Object.keys(settings.hooks).length) delete settings.hooks;
28
+ }
29
+ }
30
+
10
31
  const args = process.argv.slice(2);
11
32
 
12
33
  if (args.length === 0) {
@@ -20,17 +41,21 @@ if (args.includes('--help') || args.includes('-h')) {
20
41
  jarvis — Claude Code terminal dashboard + semantic memory graph
21
42
 
22
43
  Usage:
23
- jarvis Show version
24
- jarvis --usage Show full usage dashboard
25
- jarvis --watch Refresh dashboard every 30s
26
- jarvis --setup Install status bar into ~/.claude/settings.json
27
- jarvis --graph Open Neo4j browser at http://localhost:7474
28
- jarvis --line Single-line status (for Claude Code status bar)
29
- jarvis --help Show this help
44
+ jarvis Show version
45
+ jarvis --usage Show full usage dashboard
46
+ jarvis --watch Refresh dashboard every 30s
47
+ jarvis --setup Install status bar, skills and default trigger (session)
48
+ jarvis --graph Open Neo4j browser at http://localhost:7474
49
+ jarvis --line Single-line status (for Claude Code status bar)
50
+ jarvis --trigger Show current trigger mode
51
+ jarvis --trigger session Hook runs once per session (default)
52
+ jarvis --trigger prompt Hook runs on every prompt
53
+ jarvis --trigger off Disable automatic memory loading
54
+ jarvis --help Show this help
30
55
 
31
56
  Slash commands (inside Claude Code):
32
- /setup-memory Setup Docker + Neo4j + register MCP server
33
- /memory-index Index a repository into the memory graph
57
+ /setup-memory Setup Docker + Neo4j + register MCP server
58
+ /memory-index Index a repository into the memory graph
34
59
 
35
60
  Data source: ~/.claude/projects/
36
61
  `);
@@ -60,16 +85,63 @@ if (args.includes('--graph')) {
60
85
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
61
86
  console.log(' ✓ Status bar configured');
62
87
 
63
- // Slash commands → ~/.claude/commands/
64
- const commandsDir = join(homedir(), '.claude', 'commands');
65
- mkdirSync(commandsDir, { recursive: true });
66
- const srcCommands = join(__dir, '../.claude/commands');
67
- for (const file of ['setup-memory.md', 'memory-index.md']) {
68
- copyFileSync(join(srcCommands, file), join(commandsDir, file));
69
- console.log(` ✓ Slash command /${file.replace('.md', '')} installed`);
88
+ // Slash commands → ~/.claude/skills/<name>/SKILL.md
89
+ const skillsDir = join(homedir(), '.claude', 'skills');
90
+ const srcSkills = join(__dir, '../.claude/skills');
91
+ for (const skill of ['setup-memory', 'memory-index']) {
92
+ const destDir = join(skillsDir, skill);
93
+ mkdirSync(destDir, { recursive: true });
94
+ copyFileSync(join(srcSkills, skill, 'SKILL.md'), join(destDir, 'SKILL.md'));
95
+ console.log(` ✓ Slash command /${skill} installed`);
96
+ }
97
+
98
+ // Trigger padrão: session
99
+ const memoryCfg = loadMemoryConfig();
100
+ if (!memoryCfg.trigger) {
101
+ memoryCfg.trigger = 'session';
102
+ saveMemoryConfig(memoryCfg);
103
+ setHook(settings, true);
104
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
105
+ console.log(' ✓ Memory trigger set to: session');
70
106
  }
71
107
 
72
108
  console.log('\n Restart Claude Code to activate.\n');
109
+ } else if (args.includes('--trigger')) {
110
+ const mode = args[args.indexOf('--trigger') + 1];
111
+ const validModes = ['session', 'prompt', 'off'];
112
+
113
+ if (!mode || !validModes.includes(mode)) {
114
+ const cfg = loadMemoryConfig();
115
+ const current = cfg.trigger || 'session';
116
+ console.log(`\n Trigger mode: ${current}\n`);
117
+ console.log(` Usage: jarvis --trigger <session|prompt|off>\n`);
118
+ process.exit(0);
119
+ }
120
+
121
+ const cfg = loadMemoryConfig();
122
+ cfg.trigger = mode;
123
+ saveMemoryConfig(cfg);
124
+
125
+ const settingsPath = join(homedir(), '.claude', 'settings.json');
126
+ let settings = {};
127
+ if (existsSync(settingsPath)) {
128
+ try { settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch { /* keep empty */ }
129
+ }
130
+ setHook(settings, mode !== 'off');
131
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
132
+
133
+ const icons = { session: '⬡', prompt: '⬡⬡', off: '○' };
134
+ console.log(`\n ${icons[mode]} Trigger mode set to: ${mode}`);
135
+ if (mode !== 'off') console.log(` Hook configured in ~/.claude/settings.json`);
136
+ else console.log(` Hook removed from ~/.claude/settings.json`);
137
+ console.log(`\n Restart Claude Code to activate.\n`);
138
+ } else if (args.includes('--query')) {
139
+ try {
140
+ const { queryByPath } = await import('../src/memory/query-by-path.js');
141
+ const result = await queryByPath(process.cwd());
142
+ if (result) process.stdout.write(result + '\n');
143
+ } catch { /* silencioso — hook nunca deve quebrar a sessão */ }
144
+ process.exit(0);
73
145
  } else if (args.includes('--line') || args.includes('-l')) {
74
146
  renderLine();
75
147
  } else if (args.includes('--watch') || args.includes('-w')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gabrihhh/jarvis",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
4
4
  "description": "Claude Code terminal dashboard + semantic memory graph via Neo4j",
5
5
  "bin": {
6
6
  "jarvis": "bin/jarvis.js",
@@ -13,7 +13,7 @@
13
13
  "files": [
14
14
  "bin/",
15
15
  "src/",
16
- ".claude/commands/"
16
+ ".claude/skills/"
17
17
  ],
18
18
  "keywords": [
19
19
  "claude",
@@ -5,6 +5,7 @@ import {
5
5
  ListToolsRequestSchema,
6
6
  } from '@modelcontextprotocol/sdk/types.js';
7
7
  import { runQuery, runWriteQuery, closeDriver } from './neo4j-client.js';
8
+ import { queryByPath } from './query-by-path.js';
8
9
 
9
10
  // ── Tool definitions ──────────────────────────────────────────────────────────
10
11
 
@@ -26,6 +27,17 @@ const TOOLS = [
26
27
  required: ['name', 'branch'],
27
28
  },
28
29
  },
30
+ {
31
+ name: 'query-by-path',
32
+ description: 'Retorna o projeto indexado cujo path corresponde ao diretório informado. Use no início de cada sessão passando o cwd.',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ path: { type: 'string', description: 'Path absoluto do diretório atual (cwd)' },
37
+ },
38
+ required: ['path'],
39
+ },
40
+ },
29
41
  {
30
42
  name: 'search-concept',
31
43
  description: 'Busca módulos e arquivos relacionados a um conceito de negócio.',
@@ -355,6 +367,7 @@ export async function startServer() {
355
367
  switch (name) {
356
368
  case 'list-projects': text = await handleListProjects(); break;
357
369
  case 'query-project': text = await handleQueryProject(args); break;
370
+ case 'query-by-path': { const r = await queryByPath(args.path); text = r ?? `Nenhum projeto indexado encontrado para: ${args.path}`; break; }
358
371
  case 'search-concept': text = await handleSearchConcept(args); break;
359
372
  case 'get-module-detail': text = await handleGetModuleDetail(args); break;
360
373
  case 'save-project': text = await handleSaveProject(args); break;
@@ -0,0 +1,68 @@
1
+ import { existsSync, writeFileSync, readFileSync, readdirSync, statSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { tmpdir, homedir } from 'os';
4
+ import { runQuery, closeDriver } from './neo4j-client.js';
5
+
6
+ const CONFIG_PATH = join(homedir(), '.claude-memory.json');
7
+
8
+ function loadConfig() {
9
+ try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf8')); } catch { return {}; }
10
+ }
11
+
12
+ function getCurrentSessionId() {
13
+ const sessionsDir = join(homedir(), '.claude', 'sessions');
14
+ if (!existsSync(sessionsDir)) return null;
15
+ try {
16
+ const files = readdirSync(sessionsDir)
17
+ .map(f => ({ f, mtime: statSync(join(sessionsDir, f)).mtimeMs }))
18
+ .sort((a, b) => b.mtime - a.mtime);
19
+ if (!files.length) return null;
20
+ return JSON.parse(readFileSync(join(sessionsDir, files[0].f), 'utf8'))?.sessionId || null;
21
+ } catch { return null; }
22
+ }
23
+
24
+ export async function queryByPath(cwd) {
25
+ const config = loadConfig();
26
+ const mode = config.trigger || 'session';
27
+
28
+ if (mode === 'session') {
29
+ const sessionId = getCurrentSessionId();
30
+ if (sessionId) {
31
+ const lockFile = join(tmpdir(), `jarvis-memory-${sessionId}.lock`);
32
+ if (existsSync(lockFile)) return null;
33
+ writeFileSync(lockFile, new Date().toISOString());
34
+ }
35
+ }
36
+
37
+ const records = await runQuery(`
38
+ MATCH (p:Project)
39
+ WHERE $cwd STARTS WITH p.path
40
+ OPTIONAL MATCH (p)-[:HAS_MODULE]->(m:Module)
41
+ OPTIONAL MATCH (m)-[:HANDLES]->(c:Concept)
42
+ OPTIONAL MATCH (m)-[:IMPLEMENTS]->(pat:Pattern)
43
+ RETURN p, collect(DISTINCT m) AS modules,
44
+ collect(DISTINCT c.name) AS concepts,
45
+ collect(DISTINCT pat.name) AS patterns
46
+ ORDER BY size(p.path) DESC
47
+ LIMIT 1
48
+ `, { cwd });
49
+
50
+ await closeDriver();
51
+
52
+ if (!records.length) return null;
53
+
54
+ const r = records[0];
55
+ const p = r.get('p').properties;
56
+ const modules = r.get('modules').map(m => m.properties);
57
+ const concepts = [...new Set(r.get('concepts'))].filter(Boolean);
58
+ const patterns = [...new Set(r.get('patterns'))].filter(Boolean);
59
+
60
+ return [
61
+ `## Contexto do Repositório (jarvis-memory)`,
62
+ `**Projeto:** ${p.name} [${p.branch}]${p.description ? ' — ' + p.description : ''}`,
63
+ '',
64
+ `**Módulos:** ${modules.map(m => `${m.name} (${m.domain})`).join(', ') || 'nenhum'}`,
65
+ concepts.length ? `**Conceitos:** ${concepts.join(', ')}` : '',
66
+ patterns.length ? `**Padrões:** ${patterns.join(', ')}` : '',
67
+ ].filter(Boolean).join('\n');
68
+ }
package/src/statusline.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { Chalk } from 'chalk';
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
2
5
  import { readAllUsage, getCurrentSessionFile, readCurrentSessionUsage } from './reader.js';
3
- import { aggregateStats, aggregateSession, formatCost } from './calculator.js';
6
+ import { aggregateStats, aggregateSession } from './calculator.js';
4
7
 
5
8
  const chalk = new Chalk({ level: 3 });
6
9
 
7
- const PINK = '#f472b6';
8
- const BLUE = '#60a5fa';
9
- const CYAN = '#22d3ee';
10
- const DIM = '#444444';
10
+ const PINK = '#f472b6';
11
+ const CYAN = '#22d3ee';
11
12
 
12
13
  function bar(percent, width = 8) {
13
14
  const filled = Math.round((percent / 100) * width);
@@ -15,10 +16,37 @@ function bar(percent, width = 8) {
15
16
  return '█'.repeat(filled) + '░'.repeat(empty);
16
17
  }
17
18
 
19
+ function readTriggerMode() {
20
+ try {
21
+ const cfg = JSON.parse(readFileSync(join(homedir(), '.claude-memory.json'), 'utf8'));
22
+ return cfg.trigger || 'session';
23
+ } catch { return 'off'; }
24
+ }
25
+
26
+ function buildBox(inner, color) {
27
+ return [
28
+ chalk.hex(color).bold(`╭${'─'.repeat(inner.length)}╮`),
29
+ chalk.hex(color).bold(`│${inner}│`),
30
+ chalk.hex(color).bold(`╰${'─'.repeat(inner.length)}╯`),
31
+ ];
32
+ }
33
+
34
+ function joinBoxes(left, right) {
35
+ return left[0] + right[0] + '\n' +
36
+ left[1] + right[1] + '\n' +
37
+ left[2] + right[2];
38
+ }
39
+
18
40
  export function renderLine() {
41
+ const mode = readTriggerMode();
42
+ const triggerInner = ` TRIGGER ${mode.toUpperCase()} `;
43
+ const rightBox = buildBox(triggerInner, PINK);
44
+
19
45
  const allEntries = readAllUsage();
46
+
20
47
  if (!allEntries.length) {
21
- process.stdout.write('claude: no data');
48
+ const leftBox = buildBox(` CONTEXT ${'░'.repeat(8)} 0% `, CYAN);
49
+ process.stdout.write(joinBoxes(leftBox, rightBox));
22
50
  return;
23
51
  }
24
52
 
@@ -28,30 +56,12 @@ export function renderLine() {
28
56
  const sessionEntries = sessionId ? readCurrentSessionUsage(sessionId) : [];
29
57
  const session = aggregateSession(sessionEntries);
30
58
 
31
- const { monthly, weekly, daily } = stats;
32
-
33
- const weeklyPct = monthly.total > 0 ? Math.round((weekly.total / monthly.total) * 100) : 0;
34
- const todayPct = monthly.total > 0 ? Math.round((daily.total / monthly.total) * 100) : 0;
35
-
36
59
  if (!session) {
37
- const inner = ` CONTEXT ${'░'.repeat(8)} 0% `;
38
- const top = `╭${'─'.repeat(inner.length)}╮`;
39
- const bottom = `╰${'─'.repeat(inner.length)}╯`;
40
- process.stdout.write(
41
- chalk.hex(CYAN).bold(top) + '\n' +
42
- chalk.hex(CYAN).bold(`│${inner}│`) + '\n' +
43
- chalk.hex(CYAN).bold(bottom)
44
- );
60
+ const leftBox = buildBox(` CONTEXT ${'░'.repeat(8)} 0% `, CYAN);
61
+ process.stdout.write(joinBoxes(leftBox, rightBox));
45
62
  return;
46
63
  }
47
64
 
48
- const inner = ` CONTEXT ${bar(session.percent)} ${session.percent}% `;
49
- const top = `╭${'─'.repeat(inner.length)}╮`;
50
- const bottom = `╰${'─'.repeat(inner.length)}╯`;
51
-
52
- process.stdout.write(
53
- chalk.hex(CYAN).bold(top) + '\n' +
54
- chalk.hex(CYAN).bold(`│${inner}│`) + '\n' +
55
- chalk.hex(CYAN).bold(bottom)
56
- );
65
+ const leftBox = buildBox(` CONTEXT ${bar(session.percent)} ${session.percent}% `, CYAN);
66
+ process.stdout.write(joinBoxes(leftBox, rightBox));
57
67
  }