@mmmbuto/nexuscli 0.9.6 → 0.9.7-termux

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.
@@ -5,6 +5,7 @@
5
5
  * - Claude: ~/.claude/projects/<slug>/<sessionId>.jsonl
6
6
  * - Codex : ~/.codex/sessions/<sessionId>.jsonl
7
7
  * - Gemini: ~/.gemini/sessions/<sessionId>.jsonl
8
+ * - Qwen : ~/.qwen/projects/<sanitized>/chats/<sessionId>.jsonl
8
9
  *
9
10
  * Note:
10
11
  * - Usa FILESYSTEM come source of truth: non legge contenuti, solo metadati.
@@ -22,19 +23,21 @@ const HOME = process.env.HOME || '';
22
23
  const CLAUDE_PROJECTS = path.join(HOME, '.claude', 'projects');
23
24
  const CODEX_SESSIONS = path.join(HOME, '.codex', 'sessions');
24
25
  const GEMINI_SESSIONS = path.join(HOME, '.gemini', 'sessions');
26
+ const QWEN_PROJECTS = path.join(HOME, '.qwen', 'projects');
25
27
 
26
28
  class SessionImporter {
27
29
  constructor() {}
28
30
 
29
31
  /**
30
32
  * Importa tutte le sessioni per tutti gli engine
31
- * @returns {{claude:number, codex:number, gemini:number}}
33
+ * @returns {{claude:number, codex:number, gemini:number, qwen:number}}
32
34
  */
33
35
  importAll() {
34
36
  const claude = this.importClaudeSessions();
35
37
  const codex = this.importCodexSessions();
36
38
  const gemini = this.importGeminiSessions();
37
- return { claude, codex, gemini };
39
+ const qwen = this.importQwenSessions();
40
+ return { claude, codex, gemini, qwen };
38
41
  }
39
42
 
40
43
  /**
@@ -110,6 +113,36 @@ class SessionImporter {
110
113
  return imported;
111
114
  }
112
115
 
116
+ /**
117
+ * Qwen: ~/.qwen/projects/<sanitized>/chats/<sessionId>.jsonl
118
+ */
119
+ importQwenSessions() {
120
+ let imported = 0;
121
+ if (!fs.existsSync(QWEN_PROJECTS)) return imported;
122
+
123
+ const projects = fs.readdirSync(QWEN_PROJECTS);
124
+ for (const project of projects) {
125
+ const projectDir = path.join(QWEN_PROJECTS, project);
126
+ if (!fs.statSync(projectDir).isDirectory()) continue;
127
+
128
+ const chatsDir = path.join(projectDir, 'chats');
129
+ if (!fs.existsSync(chatsDir)) continue;
130
+
131
+ const files = fs.readdirSync(chatsDir).filter(f => f.endsWith('.jsonl'));
132
+ for (const file of files) {
133
+ const sessionId = file.replace('.jsonl', '');
134
+ if (this.sessionExists(sessionId)) continue;
135
+
136
+ this.insertSession(sessionId, 'qwen', '', null);
137
+ imported++;
138
+ }
139
+ }
140
+
141
+ if (imported > 0) saveDb();
142
+ console.log(`[SessionImporter] Qwen imported: ${imported}`);
143
+ return imported;
144
+ }
145
+
113
146
  /**
114
147
  * Inserisce riga minima in sessions
115
148
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * SessionManager - Session Sync Pattern Implementation (TRI CLI v0.4.0)
3
3
  *
4
- * Simplified session management for Claude/Codex/Gemini engines.
4
+ * Simplified session management for Claude/Codex/Gemini/Qwen engines.
5
5
  * Principle: FILESYSTEM = SOURCE OF TRUTH
6
6
  *
7
7
  * Flow:
@@ -22,6 +22,7 @@ const SESSION_DIRS = {
22
22
  claude: path.join(process.env.HOME || '', '.claude', 'projects'),
23
23
  codex: path.join(process.env.HOME || '', '.codex', 'sessions'),
24
24
  gemini: path.join(process.env.HOME || '', '.gemini', 'sessions'),
25
+ qwen: path.join(process.env.HOME || '', '.qwen', 'projects'),
25
26
  };
26
27
 
27
28
  class SessionManager {
@@ -52,9 +53,9 @@ class SessionManager {
52
53
  sessionFileExists(sessionId, engine, workspacePath) {
53
54
  const normalizedEngine = this._normalizeEngine(engine);
54
55
 
55
- // Codex/Gemini exec mode doesn't create session files - trust DB mapping
56
+ // Codex/Gemini/Qwen exec mode doesn't require filesystem checks - trust DB mapping
56
57
  // Session continuity is managed via NexusCLI's message DB + contextBridge
57
- if (normalizedEngine === 'codex' || normalizedEngine === 'gemini') {
58
+ if (normalizedEngine === 'codex' || normalizedEngine === 'gemini' || normalizedEngine === 'qwen') {
58
59
  return true; // Always trust DB for exec-mode CLI sessions
59
60
  }
60
61
 
@@ -108,6 +109,11 @@ class SessionManager {
108
109
  case 'gemini':
109
110
  // Gemini sessions: ~/.gemini/sessions/<sessionId>.jsonl
110
111
  return path.join(SESSION_DIRS.gemini, `${sessionId}.jsonl`);
112
+ case 'qwen': {
113
+ // Qwen sessions: ~/.qwen/projects/<sanitized-cwd>/chats/<sessionId>.jsonl
114
+ const project = this._pathToQwenProject(workspacePath);
115
+ return path.join(SESSION_DIRS.qwen, project, 'chats', `${sessionId}.jsonl`);
116
+ }
111
117
 
112
118
  default:
113
119
  console.warn(`[SessionManager] Unknown engine: ${engine}`);
@@ -124,6 +130,7 @@ class SessionManager {
124
130
  if (lower.includes('claude')) return 'claude';
125
131
  if (lower.includes('codex') || lower.includes('openai')) return 'codex';
126
132
  if (lower.includes('gemini') || lower.includes('google')) return 'gemini';
133
+ if (lower.includes('qwen')) return 'qwen';
127
134
  return lower;
128
135
  }
129
136
 
@@ -138,6 +145,15 @@ class SessionManager {
138
145
  return workspacePath.replace(/\//g, '-');
139
146
  }
140
147
 
148
+ /**
149
+ * Convert workspace path to Qwen project dir (matches Qwen Storage.sanitizeCwd)
150
+ * Replaces non-alphanumeric characters with '-'
151
+ */
152
+ _pathToQwenProject(workspacePath) {
153
+ if (!workspacePath) return 'default';
154
+ return workspacePath.replace(/[^a-zA-Z0-9]/g, '-');
155
+ }
156
+
141
157
  /**
142
158
  * Generate workspace hash (legacy method, kept for compatibility)
143
159
  * @deprecated Use _pathToSlug instead (matches Claude Code behavior)
@@ -325,7 +341,12 @@ class SessionManager {
325
341
  this.lastAccess.delete(cacheKey);
326
342
 
327
343
  // Delete the original .jsonl file (SYNC DELETE)
328
- const sessionFile = this._getSessionFilePath(session.id, session.engine, session.workspace_path);
344
+ const sessionFile = this._getSessionFilePath(
345
+ session.id,
346
+ session.engine,
347
+ session.workspace_path,
348
+ session.session_path
349
+ );
329
350
  if (sessionFile && fs.existsSync(sessionFile)) {
330
351
  try {
331
352
  fs.unlinkSync(sessionFile);
@@ -353,10 +374,11 @@ class SessionManager {
353
374
  /**
354
375
  * Get the filesystem path for a session file
355
376
  */
356
- _getSessionFilePath(sessionId, engine, workspacePath) {
377
+ _getSessionFilePath(sessionId, engine, workspacePath, sessionPath) {
357
378
  const normalizedEngine = engine?.toLowerCase().includes('claude') ? 'claude'
358
379
  : engine?.toLowerCase().includes('codex') ? 'codex'
359
380
  : engine?.toLowerCase().includes('gemini') ? 'gemini'
381
+ : engine?.toLowerCase().includes('qwen') ? 'qwen'
360
382
  : 'claude';
361
383
 
362
384
  switch (normalizedEngine) {
@@ -367,6 +389,11 @@ class SessionManager {
367
389
  return path.join(SESSION_DIRS.codex, `${sessionId}.jsonl`);
368
390
  case 'gemini':
369
391
  return path.join(SESSION_DIRS.gemini, `${sessionId}.jsonl`);
392
+ case 'qwen': {
393
+ const project = this._pathToQwenProject(workspacePath);
394
+ const fileId = sessionPath || sessionId;
395
+ return path.join(SESSION_DIRS.qwen, project, 'chats', `${fileId}.jsonl`);
396
+ }
370
397
  default:
371
398
  return null;
372
399
  }
@@ -4,6 +4,7 @@ const os = require('os');
4
4
 
5
5
  describe('HistorySync', () => {
6
6
  let tmpDir;
7
+ let testHome;
7
8
  let historyPath;
8
9
  let HistorySync;
9
10
  let initDb;
@@ -26,7 +27,8 @@ describe('HistorySync', () => {
26
27
  beforeEach(async () => {
27
28
  // Fresh temp DB per test
28
29
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nexus-db-'));
29
- process.env.NEXUSCLI_DB_DIR = tmpDir;
30
+ testHome = fs.mkdtempSync(path.join(os.tmpdir(), 'nexuscli-home-'));
31
+ process.env.HOME = testHome;
30
32
 
31
33
  jest.resetModules();
32
34
  ({ initDb, prepare, getDb } = require('../db'));
@@ -83,7 +85,14 @@ describe('HistorySync', () => {
83
85
  test('getWorkspaceSessions filters by workspace path', async () => {
84
86
  await historySync.sync(true);
85
87
 
86
- const sessions = await historySync.getWorkspaceSessions('/test/path');
88
+ const grouped = await historySync.getWorkspaceSessions('/test/path');
89
+ const sessions = [
90
+ ...grouped.today,
91
+ ...grouped.yesterday,
92
+ ...grouped.last7days,
93
+ ...grouped.last30days,
94
+ ...grouped.older
95
+ ];
87
96
  expect(sessions).toHaveLength(1);
88
97
  expect(sessions[0].id).toBe('test-1');
89
98
  });
@@ -9,16 +9,34 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
- const HistorySync = require('../services/history-sync');
13
- const { initDb, getDb, prepare } = require('../db');
12
+ const os = require('os');
13
+
14
+ let HistorySync;
15
+ let initDb;
16
+ let getDb;
17
+ let prepare;
14
18
 
15
19
  describe('Session Sync Integration', () => {
16
20
  let historySync;
17
21
  let testHistoryPath;
18
22
 
19
23
  beforeAll(async () => {
24
+ const testHome = fs.mkdtempSync(path.join(os.tmpdir(), 'nexuscli-home-'));
25
+ process.env.HOME = testHome;
26
+
27
+ jest.resetModules();
28
+ HistorySync = require('../services/history-sync');
29
+ ({ initDb, getDb, prepare } = require('../db'));
30
+
20
31
  // Setup in-memory test database
21
- await initDb({ skipMigrationCheck: true });
32
+ await initDb();
33
+ });
34
+
35
+ afterAll(() => {
36
+ const db = getDb && getDb();
37
+ if (db && typeof db.close === 'function') {
38
+ db.close();
39
+ }
22
40
  });
23
41
 
24
42
  beforeEach(() => {
@@ -110,11 +128,25 @@ describe('Session Sync Integration', () => {
110
128
  const workspace1Sessions = await historySync.getWorkspaceSessions('/test/workspace1');
111
129
  const workspace2Sessions = await historySync.getWorkspaceSessions('/test/workspace2');
112
130
 
113
- expect(workspace1Sessions).toHaveLength(1);
114
- expect(workspace1Sessions[0].id).toBe('session-001');
115
-
116
- expect(workspace2Sessions).toHaveLength(1);
117
- expect(workspace2Sessions[0].id).toBe('session-002');
131
+ const ws1 = [
132
+ ...workspace1Sessions.today,
133
+ ...workspace1Sessions.yesterday,
134
+ ...workspace1Sessions.last7days,
135
+ ...workspace1Sessions.last30days,
136
+ ...workspace1Sessions.older
137
+ ];
138
+ expect(ws1).toHaveLength(1);
139
+ expect(ws1[0].id).toBe('session-001');
140
+
141
+ const ws2 = [
142
+ ...workspace2Sessions.today,
143
+ ...workspace2Sessions.yesterday,
144
+ ...workspace2Sessions.last7days,
145
+ ...workspace2Sessions.last30days,
146
+ ...workspace2Sessions.older
147
+ ];
148
+ expect(ws2).toHaveLength(1);
149
+ expect(ws2[0].id).toBe('session-002');
118
150
  });
119
151
 
120
152
  test('conversation titles are generated from first message', async () => {
@@ -3,38 +3,56 @@
3
3
  * Phase 7 - End-to-end flow validation
4
4
  */
5
5
 
6
- const { prepare } = require('../db');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const TEST_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'nexuscli-home-'));
11
+ process.env.HOME = TEST_HOME;
12
+
13
+ let initDb;
14
+ let getDb;
15
+ let prepare;
16
+
17
+ beforeAll(async () => {
18
+ jest.resetModules();
19
+ ({ initDb, getDb, prepare } = require('../db'));
20
+ await initDb(); // run migrations for session tables
21
+ });
22
+
23
+ afterAll(() => {
24
+ const db = getDb && getDb();
25
+ if (db && typeof db.close === 'function') {
26
+ db.close();
27
+ }
28
+ });
7
29
 
8
30
  describe('Database Integration', () => {
9
31
  test('should initialize database successfully', async () => {
10
- const db = await prepare();
32
+ const db = getDb();
11
33
  expect(db).toBeDefined();
12
34
  expect(typeof db.exec).toBe('function');
13
35
  });
14
36
 
15
37
  test('should have sessions table', async () => {
16
- const db = await prepare();
17
- const result = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'");
18
- expect(result.length).toBeGreaterThan(0);
38
+ const row = prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'").get();
39
+ expect(row).not.toBeNull();
19
40
  });
20
41
 
21
42
  test('should have session_summaries table', async () => {
22
- const db = await prepare();
23
- const result = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='session_summaries'");
24
- expect(result.length).toBeGreaterThan(0);
43
+ const row = prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='session_summaries'").get();
44
+ expect(row).not.toBeNull();
25
45
  });
26
46
 
27
47
  test('should have workspace_memory table', async () => {
28
- const db = await prepare();
29
- const result = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='workspace_memory'");
30
- expect(result.length).toBeGreaterThan(0);
48
+ const row = prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='workspace_memory'").get();
49
+ expect(row).not.toBeNull();
31
50
  });
32
51
 
33
52
  test('should query sessions successfully', async () => {
34
- const db = await prepare();
35
- const result = db.exec("SELECT COUNT(*) as count FROM sessions");
36
- expect(result.length).toBeGreaterThan(0);
37
- expect(result[0].columns).toContain('count');
53
+ const row = prepare('SELECT COUNT(*) as count FROM sessions').get();
54
+ expect(row).toBeDefined();
55
+ expect(Object.prototype.hasOwnProperty.call(row, 'count')).toBe(true);
38
56
  });
39
57
  });
40
58
 
@@ -47,7 +65,6 @@ describe('Service Integration', () => {
47
65
  const loader = new CliLoader();
48
66
 
49
67
  expect(manager.claudePath).toBe(loader.claudePath);
50
- expect(manager.historyPath).toBe(loader.historyPath);
51
68
  });
52
69
 
53
70
  test('All services should initialize without errors', () => {
@@ -3,6 +3,13 @@
3
3
  * Phase 7 - Performance benchmarks
4
4
  */
5
5
 
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const TEST_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'nexuscli-home-'));
11
+ process.env.HOME = TEST_HOME;
12
+
6
13
  const WorkspaceManager = require('../services/workspace-manager');
7
14
  const SummaryGenerator = require('../services/summary-generator');
8
15
 
@@ -45,7 +52,9 @@ describe('Performance Benchmarks', () => {
45
52
 
46
53
  test('Workspace validation should be fast', async () => {
47
54
  const manager = new WorkspaceManager();
48
- const testPath = '/var/www/myapp';
55
+ const baseDir = path.join(process.env.HOME || os.tmpdir(), '.nexuscli-test');
56
+ fs.mkdirSync(baseDir, { recursive: true });
57
+ const testPath = fs.mkdtempSync(path.join(baseDir, 'ws-'));
49
58
 
50
59
  const start = Date.now();
51
60
  const validated = await manager.validateWorkspace(testPath);
@@ -97,18 +106,16 @@ describe('Memory Efficiency', () => {
97
106
  });
98
107
 
99
108
  describe('Ultra-Light Compliance', () => {
100
- test('should not store assistant messages in DB (code verification)', () => {
101
- const fs = require('fs');
102
- const chatCode = fs.readFileSync('routes/chat.js', 'utf8');
109
+ test('should persist both user and assistant messages (code verification)', () => {
110
+ const chatCode = fs.readFileSync(path.join(__dirname, '..', 'routes', 'chat.js'), 'utf8');
103
111
 
104
- // Verify comment exists about assistant messages
105
- expect(chatCode).toContain('assistant replies stay in CLI files');
106
- expect(chatCode).toContain('User message saved');
112
+ // Verify both user and assistant messages are persisted
113
+ expect(chatCode).toContain('[Chat] Saved user message');
114
+ expect(chatCode).toContain('[Chat] Saved assistant message');
107
115
  });
108
116
 
109
117
  test('should use cache for history.jsonl reads', () => {
110
- const fs = require('fs');
111
- const managerCode = fs.readFileSync('services/workspace-manager.js', 'utf8');
118
+ const managerCode = fs.readFileSync(path.join(__dirname, '..', 'services', 'workspace-manager.js'), 'utf8');
112
119
 
113
120
  // Verify cache implementation exists
114
121
  expect(managerCode).toContain('historyCache');
@@ -3,6 +3,13 @@
3
3
  * Phase 7 - Testing & Deployment
4
4
  */
5
5
 
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const TEST_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'nexuscli-home-'));
11
+ process.env.HOME = TEST_HOME;
12
+
6
13
  const WorkspaceManager = require('../services/workspace-manager');
7
14
  const CliLoader = require('../services/cli-loader');
8
15
  const SummaryGenerator = require('../services/summary-generator');
@@ -15,8 +22,10 @@ describe('WorkspaceManager', () => {
15
22
  });
16
23
 
17
24
  test('should validate workspace path', async () => {
18
- // Test with allowed path
19
- const validPath = '/var/www/myapp';
25
+ // Test with allowed path (Termux-first)
26
+ const baseDir = path.join(process.env.HOME || os.tmpdir(), '.nexuscli-test');
27
+ fs.mkdirSync(baseDir, { recursive: true });
28
+ const validPath = fs.mkdtempSync(path.join(baseDir, 'ws-'));
20
29
  const result = await manager.validateWorkspace(validPath);
21
30
  expect(result).toBe(validPath);
22
31
  });
@@ -28,7 +37,8 @@ describe('WorkspaceManager', () => {
28
37
  });
29
38
 
30
39
  test('should detect non-existent workspace', async () => {
31
- const nonExistent = '/var/nonexistent-workspace-12345';
40
+ const baseDir = path.join(process.env.HOME || os.tmpdir(), '.nexuscli-test');
41
+ const nonExistent = path.join(baseDir, 'nonexistent-workspace-12345');
32
42
  await expect(manager.validateWorkspace(nonExistent)).rejects.toThrow('does not exist');
33
43
  });
34
44
 
@@ -41,7 +51,7 @@ describe('WorkspaceManager', () => {
41
51
 
42
52
  test('should extract title from messages', () => {
43
53
  const messages = [
44
- { display: 'Implement user authentication feature' },
54
+ { role: 'user', display: 'Implement user authentication feature' },
45
55
  { display: 'Follow up on previous discussion' }
46
56
  ];
47
57
  const title = manager.extractTitle(messages);
@@ -62,11 +72,6 @@ describe('CliLoader', () => {
62
72
  expect(loader.claudePath).toBe(expectedPath);
63
73
  });
64
74
 
65
- test('should initialize with correct historyPath', () => {
66
- const expectedPath = require('path').join(process.env.HOME, '.claude', 'history.jsonl');
67
- expect(loader.historyPath).toBe(expectedPath);
68
- });
69
-
70
75
  test('should have loadMessagesFromCLI method', () => {
71
76
  expect(typeof loader.loadMessagesFromCLI).toBe('function');
72
77
  });
@@ -147,7 +152,9 @@ describe('SummaryGenerator', () => {
147
152
  describe('Integration - Service Interactions', () => {
148
153
  test('WorkspaceManager should use consistent path resolution', async () => {
149
154
  const manager = new WorkspaceManager();
150
- const testPath = '/var/www/myapp';
155
+ const baseDir = path.join(process.env.HOME || os.tmpdir(), '.nexuscli-test');
156
+ fs.mkdirSync(baseDir, { recursive: true });
157
+ const testPath = fs.mkdtempSync(path.join(baseDir, 'ws-'));
151
158
  const validated = await manager.validateWorkspace(testPath);
152
159
  expect(validated).toBe(testPath);
153
160
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscli",
3
- "version": "0.9.6",
4
- "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini)",
3
+ "version": "0.9.7-termux",
4
+ "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini/Qwen)",
5
5
  "main": "lib/server/server.js",
6
6
  "bin": {
7
7
  "nexuscli": "bin/nexuscli.js"