@mmmbuto/nexuscrew 0.2.1 → 0.2.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.
Files changed (52) hide show
  1. package/README.md +122 -67
  2. package/bin/nexuscrew.js +194 -470
  3. package/frontend/dist/assets/{index-BG1N7bSL.js → index-BsldfYnr.js} +1704 -1711
  4. package/frontend/dist/assets/index-DcQGg3dp.css +1 -0
  5. package/frontend/dist/index.html +3 -11
  6. package/lib/server/routes/hosts.js +22 -12
  7. package/lib/server/routes/launchers.js +12 -0
  8. package/lib/server/routes/models.js +16 -63
  9. package/lib/server/routes/runtime.js +10 -0
  10. package/lib/server/routes/send.js +97 -258
  11. package/lib/server/routes/sessions.js +13 -42
  12. package/lib/server/routes/status.js +18 -22
  13. package/lib/server/routes/tmux.js +88 -0
  14. package/lib/server/server.js +60 -81
  15. package/lib/services/engine-discovery.js +192 -445
  16. package/lib/services/log-watcher.js +22 -40
  17. package/lib/services/session-store.js +88 -62
  18. package/lib/services/tmux-manager.js +111 -173
  19. package/package.json +5 -9
  20. package/frontend/dist/apple-touch-icon.png +0 -0
  21. package/frontend/dist/assets/index-CiAtinNP.css +0 -1
  22. package/frontend/dist/favicon.svg +0 -20
  23. package/frontend/dist/icon-192.png +0 -0
  24. package/frontend/dist/icon-512.png +0 -0
  25. package/frontend/dist/site.webmanifest +0 -29
  26. package/lib/config/hosts.js +0 -67
  27. package/lib/config/manager.js +0 -379
  28. package/lib/config/models.js +0 -408
  29. package/lib/server/db/adapter.js +0 -274
  30. package/lib/server/db/drivers/sql-js.js +0 -75
  31. package/lib/server/db/migrate.js +0 -174
  32. package/lib/server/db/migrations/001_base_schema.sql +0 -70
  33. package/lib/server/middleware/auth.js +0 -134
  34. package/lib/server/middleware/rate-limit.js +0 -63
  35. package/lib/server/models/User.js +0 -128
  36. package/lib/server/routes/auth.js +0 -168
  37. package/lib/server/routes/keys.js +0 -28
  38. package/lib/server/routes/runtimes.js +0 -34
  39. package/lib/server/routes/speech.js +0 -46
  40. package/lib/server/routes/upload.js +0 -135
  41. package/lib/server/routes/wake-lock.js +0 -95
  42. package/lib/server/routes/workspaces.js +0 -101
  43. package/lib/server/services/attachment-manager.js +0 -57
  44. package/lib/server/services/context-bridge.js +0 -425
  45. package/lib/server/services/runtime-manager.js +0 -462
  46. package/lib/server/services/speech-manager.js +0 -76
  47. package/lib/server/services/summary-generator.js +0 -309
  48. package/lib/server/services/workspace-manager.js +0 -79
  49. package/lib/services/remote-pane-watcher.js +0 -155
  50. package/lib/setup/postinstall.js +0 -38
  51. package/lib/utils/paths.js +0 -124
  52. package/lib/utils/termux.js +0 -182
@@ -1,309 +0,0 @@
1
- /**
2
- * SummaryGenerator - produce contextual summaries for sessions.
3
- *
4
- * Features:
5
- * - Generate AI titles for new conversations
6
- * - Generate contextual summaries for sessions
7
- * - Auto-save summaries to session_summaries table
8
- * - Support for summary-based context bridging
9
- *
10
- * Note: AI generation requires external provider configuration.
11
- * This implementation provides the structure; actual AI calls
12
- * can be integrated via environment-provided clients or webhooks.
13
- */
14
-
15
- const { v4: uuidv4 } = require('uuid');
16
- const { prepare, saveDb } = require('../db/adapter');
17
-
18
- class SummaryGenerator {
19
- constructor(options = {}) {
20
- this.options = options;
21
- // AI client can be injected or configured later
22
- this.aiClient = options.aiClient || null;
23
- }
24
-
25
- /**
26
- * Set AI client for summary generation
27
- * @param {Object} client - AI client with sendMessage method
28
- */
29
- setAIClient(client) {
30
- this.aiClient = client;
31
- }
32
-
33
- /**
34
- * Generate a short title for a new conversation (3-8 words)
35
- * Called after first AI response, runs in background
36
- * @param {string} userMessage - First user message
37
- * @param {string} assistantResponse - First AI response (optional, for context)
38
- * @returns {Promise<string>} Generated title
39
- */
40
- async generateTitle(userMessage, assistantResponse = '') {
41
- const startedAt = Date.now();
42
-
43
- // Keep context brief for fast generation
44
- const context = assistantResponse
45
- ? `User: ${userMessage.slice(0, 500)}\nAssistant: ${assistantResponse.slice(0, 500)}`
46
- : `User: ${userMessage.slice(0, 800)}`;
47
-
48
- const prompt = `Generate a brief title (3-8 words, no quotes) for this conversation:
49
-
50
- ${context}
51
-
52
- Reply with ONLY the title, nothing else.`;
53
-
54
- try {
55
- if (!this.aiClient) {
56
- console.warn('[SummaryGenerator] No AI client configured, using fallback title generation');
57
- return this.fallbackTitle(userMessage);
58
- }
59
-
60
- const result = await this.aiClient.sendMessage({
61
- prompt,
62
- conversationId: uuidv4(), // Ephemeral session
63
- model: 'haiku',
64
- onStatus: () => {} // Silence status
65
- });
66
-
67
- // Clean up the response
68
- let title = result.text.trim()
69
- .replace(/^["']|["']$/g, '') // Remove surrounding quotes
70
- .replace(/^Title:\s*/i, '') // Remove "Title:" prefix
71
- .slice(0, 60); // Max 60 chars
72
-
73
- console.log(`[SummaryGenerator] Title generated in ${Date.now() - startedAt}ms: ${title}`);
74
- return title || 'New Chat';
75
- } catch (error) {
76
- console.error('[SummaryGenerator] Title generation failed:', error.message);
77
- // Fallback to truncated first message
78
- return this.fallbackTitle(userMessage);
79
- }
80
- }
81
-
82
- /**
83
- * Fallback title generation (simple truncation)
84
- * @param {string} userMessage
85
- * @returns {string}
86
- */
87
- fallbackTitle(userMessage) {
88
- return userMessage.slice(0, 50).trim() + (userMessage.length > 50 ? '...' : '');
89
- }
90
-
91
- /**
92
- * Generate or refresh summary for a session.
93
- * @param {Object} params
94
- * @param {string} params.conversationId
95
- * @param {Array} params.messages - Array of {role, content, created_at}
96
- * @param {Object|null} params.existingSummary
97
- * @returns {Promise<Object>} summary payload
98
- */
99
- async generateSummary({ conversationId, messages = [], existingSummary = null }) {
100
- if (!conversationId) throw new Error('conversationId is required');
101
-
102
- const startedAt = Date.now();
103
-
104
- // Build transcript snippet (limit to 40 latest messages for brevity)
105
- const transcript = this.buildTranscript(messages.slice(-40));
106
-
107
- const prompt = this.buildPrompt({ conversationId, transcript, existingSummary });
108
-
109
- if (!this.aiClient) {
110
- console.warn('[SummaryGenerator] No AI client configured, using fallback summary');
111
- return this.fallbackSummary(messages);
112
- }
113
-
114
- const result = await this.aiClient.sendMessage({
115
- prompt,
116
- conversationId: uuidv4(), // Ephemeral session
117
- model: 'haiku',
118
- onStatus: () => {} // silence status for summaries
119
- });
120
-
121
- const parsed = this.safeParseJson(result.text);
122
-
123
- console.log(`[SummaryGenerator] Summary for ${conversationId} took ${Date.now() - startedAt}ms`);
124
-
125
- return parsed;
126
- }
127
-
128
- /**
129
- * Fallback summary generation (rule-based)
130
- * @param {Array} messages
131
- * @returns {Object}
132
- */
133
- fallbackSummary(messages) {
134
- const userMessages = messages.filter(m => m.role === 'user');
135
- const assistantMessages = messages.filter(m => m.role === 'assistant');
136
-
137
- // Extract first user message as summary
138
- const firstUserMsg = userMessages[0]?.content || '';
139
- const summary = firstUserMsg.slice(0, 200) + (firstUserMsg.length > 200 ? '...' : '');
140
-
141
- return {
142
- summary_short: summary,
143
- summary_long: null,
144
- key_decisions: [],
145
- tools_used: [],
146
- files_modified: []
147
- };
148
- }
149
-
150
- /**
151
- * Save summary to database
152
- * @param {string} conversationId
153
- * @param {Object} summary - {summary_short, summary_long, key_decisions, tools_used, files_modified}
154
- * @returns {boolean} Success
155
- */
156
- saveSummary(conversationId, summary) {
157
- try {
158
- const stmt = prepare(`
159
- INSERT OR REPLACE INTO session_summaries
160
- (conversation_id, summary_short, key_decisions, files_modified, updated_at)
161
- VALUES (?, ?, ?, ?, ?)
162
- `);
163
-
164
- stmt.run(
165
- conversationId,
166
- summary.summary_short || '',
167
- JSON.stringify(summary.key_decisions || []),
168
- JSON.stringify(summary.files_modified || []),
169
- Date.now()
170
- );
171
-
172
- saveDb();
173
- console.log(`[SummaryGenerator] Saved summary for ${conversationId}`);
174
- return true;
175
- } catch (error) {
176
- console.error(`[SummaryGenerator] Failed to save summary:`, error.message);
177
- return false;
178
- }
179
- }
180
-
181
- /**
182
- * Get existing summary from database
183
- * @param {string} conversationId
184
- * @returns {Object|null}
185
- */
186
- getSummary(conversationId) {
187
- try {
188
- const stmt = prepare('SELECT * FROM session_summaries WHERE conversation_id = ?');
189
- const row = stmt.get(conversationId);
190
-
191
- if (!row) return null;
192
-
193
- return {
194
- summary_short: row.summary_short,
195
- summary_long: row.summary_long || null,
196
- key_decisions: JSON.parse(row.key_decisions || '[]'),
197
- files_modified: JSON.parse(row.files_modified || '[]'),
198
- updated_at: row.updated_at
199
- };
200
- } catch (error) {
201
- console.warn(`[SummaryGenerator] Failed to get summary:`, error.message);
202
- return null;
203
- }
204
- }
205
-
206
- /**
207
- * Generate summary and save to DB (convenience method)
208
- * @param {string} conversationId
209
- * @param {Array} messages
210
- * @returns {Promise<Object|null>}
211
- */
212
- async generateAndSave(conversationId, messages) {
213
- try {
214
- const existingSummary = this.getSummary(conversationId);
215
- const summary = await this.generateSummary({ conversationId, messages, existingSummary });
216
- this.saveSummary(conversationId, summary);
217
- return summary;
218
- } catch (error) {
219
- console.error(`[SummaryGenerator] generateAndSave failed:`, error.message);
220
- return null;
221
- }
222
- }
223
-
224
- /**
225
- * Get summary text for context bridging
226
- * Returns a formatted string suitable for prefixing prompts
227
- * @param {string} conversationId
228
- * @returns {string|null}
229
- */
230
- getBridgeContext(conversationId) {
231
- const summary = this.getSummary(conversationId);
232
- if (!summary || !summary.summary_short) return null;
233
-
234
- let context = `[Session Summary]\n${summary.summary_short}`;
235
-
236
- if (summary.key_decisions && summary.key_decisions.length > 0) {
237
- context += `\n\nKey decisions:\n- ${summary.key_decisions.slice(0, 5).join('\n- ')}`;
238
- }
239
-
240
- if (summary.files_modified && summary.files_modified.length > 0) {
241
- context += `\n\nFiles worked on:\n- ${summary.files_modified.slice(0, 10).join('\n- ')}`;
242
- }
243
-
244
- return context;
245
- }
246
-
247
- buildTranscript(messages) {
248
- return messages
249
- .map(m => {
250
- const role = m.role || 'assistant';
251
- const content = (m.content || '').trim();
252
- const ts = m.created_at ? new Date(m.created_at * 1000).toISOString() : '';
253
- return `[${ts}] ${role.toUpperCase()}: ${content}`;
254
- })
255
- .join('\n')
256
- .slice(-6000); // keep prompt size reasonable
257
- }
258
-
259
- buildPrompt({ conversationId, transcript, existingSummary }) {
260
- const existing = existingSummary
261
- ? `Existing summary (for refresh): ${JSON.stringify(existingSummary)}`
262
- : 'No existing summary.';
263
-
264
- return `
265
- You are a concise assistant. Summarize the coding/chat session into JSON.
266
- Conversation ID: ${conversationId}
267
-
268
- ${existing}
269
-
270
- Provide JSON with keys:
271
- - summary_short (<=80 words)
272
- - summary_long (<=200 words, optional)
273
- - key_decisions (array of short bullet strings)
274
- - tools_used (array of tool names or commands, optional)
275
- - files_modified (array of file paths)
276
-
277
- Do not include any extra text outside valid JSON.
278
-
279
- Transcript:
280
- ${transcript}
281
- `;
282
- }
283
-
284
- safeParseJson(text) {
285
- // Try to extract JSON block
286
- const start = text.indexOf('{');
287
- const end = text.lastIndexOf('}');
288
- if (start === -1 || end === -1) {
289
- throw new Error('Failed to parse summary JSON');
290
- }
291
-
292
- const jsonString = text.slice(start, end + 1);
293
-
294
- try {
295
- const parsed = JSON.parse(jsonString);
296
- // Normalize array fields
297
- ['key_decisions', 'tools_used', 'files_modified'].forEach(key => {
298
- if (parsed[key] && !Array.isArray(parsed[key])) {
299
- parsed[key] = [parsed[key]].filter(Boolean);
300
- }
301
- });
302
- return parsed;
303
- } catch (e) {
304
- throw new Error('Invalid JSON returned by model');
305
- }
306
- }
307
- }
308
-
309
- module.exports = SummaryGenerator;
@@ -1,79 +0,0 @@
1
- /**
2
- * Workspace Manager - NexusCrew tmux-based version
3
- *
4
- * Simplified version that discovers workspaces from conversations table
5
- * instead of reading .claude/projects/ JSONL files (NexusCLI approach).
6
- */
7
-
8
- const fs = require('fs');
9
- const { prepare } = require('../db/adapter');
10
-
11
- class WorkspaceManager {
12
- /**
13
- * Discover workspaces from conversations table
14
- * Groups by workspace path and counts sessions
15
- * @returns {Array} List of workspaces with metadata
16
- */
17
- discoverWorkspaces() {
18
- const stmt = prepare(`
19
- SELECT workspace, COUNT(*) as session_count,
20
- MAX(updated_at) as last_activity
21
- FROM conversations
22
- WHERE workspace != '' AND workspace IS NOT NULL
23
- GROUP BY workspace
24
- ORDER BY last_activity DESC
25
- `);
26
-
27
- return stmt.all().map(row => ({
28
- path: row.workspace,
29
- session_count: row.session_count,
30
- last_activity: row.last_activity
31
- }));
32
- }
33
-
34
- /**
35
- * Validate workspace path exists on filesystem
36
- * @param {string} wsPath - Absolute path to workspace
37
- * @returns {boolean} True if path exists and is a directory
38
- */
39
- validateWorkspace(wsPath) {
40
- try {
41
- return fs.statSync(wsPath).isDirectory();
42
- } catch {
43
- return false;
44
- }
45
- }
46
-
47
- /**
48
- * Get conversations for a specific workspace
49
- * @param {string} workspace - Workspace path
50
- * @returns {Array} List of conversations in workspace
51
- */
52
- getWorkspaceConversations(workspace) {
53
- const stmt = prepare(`
54
- SELECT * FROM conversations
55
- WHERE workspace = ?
56
- ORDER BY updated_at DESC
57
- `);
58
- return stmt.all(workspace);
59
- }
60
-
61
- /**
62
- * Get workspace statistics
63
- * @param {string} workspace - Workspace path
64
- * @returns {Object} Statistics for the workspace
65
- */
66
- getWorkspaceStats(workspace) {
67
- const stmt = prepare(`
68
- SELECT
69
- COUNT(*) as total_conversations,
70
- SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_conversations,
71
- SUM(CASE WHEN pinned = 1 THEN 1 ELSE 0 END) as pinned_conversations
72
- FROM conversations
73
- WHERE workspace = ?
74
- `);
75
- return stmt.get(workspace);
76
- }
77
- }
78
-
79
- module.exports = WorkspaceManager;
@@ -1,155 +0,0 @@
1
- const { EventEmitter } = require('events');
2
- const LogWatcher = require('./log-watcher');
3
-
4
- function normalizeSnapshot(text = '') {
5
- return String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
6
- }
7
-
8
- function commonPrefixLength(a = '', b = '') {
9
- const max = Math.min(a.length, b.length);
10
- let index = 0;
11
- while (index < max && a[index] === b[index]) {
12
- index += 1;
13
- }
14
- return index;
15
- }
16
-
17
- class RemotePaneWatcher extends EventEmitter {
18
- constructor(tmux, options = {}) {
19
- super();
20
- this.tmux = tmux;
21
- this.parserAdapter = new LogWatcher();
22
- this.watchers = new Map();
23
- this.pollMs = options.pollMs || 400;
24
- this.idleMs = options.idleMs || 4500;
25
- this.timeoutMs = options.timeoutMs || 120000;
26
-
27
- this.parserAdapter.on('data', (event) => this.emit('data', event));
28
- }
29
-
30
- watch({ conversationId, engine, host, windowName, promptText = '', initialSnapshot = '' }) {
31
- const key = `${host?.name || host || 'local'}:${windowName}:${conversationId}`;
32
- if (this.watchers.has(key)) return key;
33
-
34
- const parserState = this.parserAdapter.createParserState(engine, conversationId);
35
- const state = {
36
- key,
37
- conversationId,
38
- engine,
39
- host,
40
- windowName,
41
- parserState,
42
- promptText: String(promptText || ''),
43
- lastSnapshot: normalizeSnapshot(initialSnapshot),
44
- startedAt: Date.now(),
45
- lastChangeAt: Date.now(),
46
- lastEmitAt: 0,
47
- stopped: false,
48
- sawAnyOutput: false
49
- };
50
-
51
- const intervalId = setInterval(() => this._poll(state), this.pollMs);
52
- state.intervalId = intervalId;
53
- this.watchers.set(key, state);
54
- return key;
55
- }
56
-
57
- unwatch(key) {
58
- const state = this.watchers.get(key);
59
- if (!state) return;
60
- clearInterval(state.intervalId);
61
- state.stopped = true;
62
- this.watchers.delete(key);
63
- }
64
-
65
- unwatchByConversation(conversationId) {
66
- for (const [key, state] of this.watchers.entries()) {
67
- if (state.conversationId === conversationId) {
68
- this.unwatch(key);
69
- }
70
- }
71
- }
72
-
73
- unwatchAll() {
74
- for (const key of this.watchers.keys()) {
75
- this.unwatch(key);
76
- }
77
- }
78
-
79
- _poll(state) {
80
- if (state.stopped) return;
81
-
82
- if (Date.now() - state.startedAt > this.timeoutMs) {
83
- this._finish(state, true);
84
- return;
85
- }
86
-
87
- const snapshot = normalizeSnapshot(
88
- this.tmux.capturePane(state.windowName, state.host, 250)
89
- );
90
-
91
- if (!snapshot) {
92
- if (Date.now() - state.lastChangeAt > this.idleMs) {
93
- this._finish(state, true);
94
- }
95
- return;
96
- }
97
-
98
- if (snapshot === state.lastSnapshot) {
99
- if (state.sawAnyOutput && Date.now() - state.lastChangeAt > this.idleMs) {
100
- this._finish(state, true);
101
- }
102
- return;
103
- }
104
-
105
- const prefixLength = commonPrefixLength(state.lastSnapshot, snapshot);
106
- let delta = snapshot.slice(prefixLength);
107
- state.lastSnapshot = snapshot;
108
- state.lastChangeAt = Date.now();
109
-
110
- if (!delta.trim()) {
111
- return;
112
- }
113
-
114
- delta = this._stripPromptEcho(delta, state);
115
- if (!delta.trim()) {
116
- return;
117
- }
118
-
119
- state.sawAnyOutput = true;
120
- this.parserAdapter.processTextChunk(delta, state.parserState);
121
- state.lastEmitAt = Date.now();
122
- }
123
-
124
- _stripPromptEcho(delta, state) {
125
- if (!state.promptText) return delta;
126
-
127
- const prompt = state.promptText.trim();
128
- if (!prompt) return delta;
129
-
130
- if (delta.includes(prompt)) {
131
- return delta.replace(prompt, '');
132
- }
133
-
134
- const compact = delta.trim();
135
- if (prompt.startsWith(compact) && compact.length > 10) {
136
- return '';
137
- }
138
-
139
- return delta;
140
- }
141
-
142
- _finish(state, emitDone = false) {
143
- this.parserAdapter.flushState(state.parserState);
144
- if (emitDone) {
145
- this.emit('data', {
146
- type: 'done',
147
- conversationId: state.conversationId,
148
- engine: state.engine
149
- });
150
- }
151
- this.unwatch(state.key);
152
- }
153
- }
154
-
155
- module.exports = RemotePaneWatcher;
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const { execSync } = require('child_process');
4
- const { ensureDirectories, PATHS } = require('../utils/paths');
5
- const { initializeConfig, isInitialized } = require('../config/manager');
6
- const { ensureHostsFile } = require('../config/hosts');
7
- const { isTermux } = require('../utils/termux');
8
-
9
- function has(command) {
10
- try {
11
- execSync(`which ${command}`, { stdio: 'ignore' });
12
- return true;
13
- } catch {
14
- return false;
15
- }
16
- }
17
-
18
- ensureDirectories();
19
- ensureHostsFile();
20
-
21
- if (!isInitialized()) {
22
- initializeConfig({ workspaces: [`${process.env.HOME || ''}/Dev`] });
23
- }
24
-
25
- console.log('');
26
- console.log('NexusCrew postinstall');
27
- console.log(` config: ${PATHS.CONFIG_FILE}`);
28
- console.log(` hosts: ${PATHS.HOSTS_FILE}`);
29
- console.log(` tmux: ${has('tmux') ? 'OK' : 'missing'}`);
30
- console.log(` ssh: ${has('ssh') ? 'OK' : 'missing'}`);
31
- console.log(` scp: ${has('scp') ? 'OK' : 'missing'}`);
32
- console.log(` whisper-cli: ${has('whisper-cli') ? 'OK' : 'missing'}`);
33
- if (isTermux()) {
34
- console.log(' platform: Termux');
35
- console.log(` termux-services: ${has('sv') ? 'OK' : 'missing'}`);
36
- }
37
- console.log(' next: run `nexuscrew setup` to configure workspace, SSH hosts, local STT, and optional Termux boot/runit');
38
- console.log('');
@@ -1,124 +0,0 @@
1
- /**
2
- * Standard Paths for NexusCrew
3
- * Termux Mobile First - all paths relative to $HOME
4
- */
5
-
6
- const fs = require('fs');
7
- const path = require('path');
8
- const os = require('os');
9
-
10
- const HOME = process.env.HOME || os.homedir();
11
-
12
- /**
13
- * NexusCrew standard paths
14
- */
15
- const PATHS = {
16
- // Main config directory
17
- CONFIG_DIR: path.join(HOME, '.nexuscrew'),
18
-
19
- // Config file
20
- CONFIG_FILE: path.join(HOME, '.nexuscrew', 'config.json'),
21
-
22
- // Data directory (database, cache)
23
- DATA_DIR: path.join(HOME, '.nexuscrew', 'data'),
24
-
25
- // Database file
26
- DATABASE: path.join(HOME, '.nexuscrew', 'data', 'nexuscrew.db'),
27
-
28
- // Logs directory
29
- LOGS_DIR: path.join(HOME, '.nexuscrew', 'logs'),
30
-
31
- // Server log
32
- SERVER_LOG: path.join(HOME, '.nexuscrew', 'logs', 'server.log'),
33
-
34
- // PID file for daemon
35
- PID_FILE: path.join(HOME, '.nexuscrew', 'nexuscrew.pid'),
36
-
37
- // Engines config directory
38
- ENGINES_DIR: path.join(HOME, '.nexuscrew', 'engines'),
39
-
40
- // Attachments directory
41
- ATTACHMENTS_DIR: path.join(HOME, '.nexuscrew', 'attachments'),
42
-
43
- // Local helper binaries
44
- BIN_DIR: path.join(HOME, '.nexuscrew', 'bin'),
45
-
46
- // Hosts registry
47
- HOSTS_FILE: path.join(HOME, '.nexuscrew', 'hosts.json'),
48
-
49
- // Termux boot script
50
- BOOT_SCRIPT: path.join(HOME, '.termux', 'boot', 'nexuscrew-start.sh'),
51
-
52
- // Termux runit service directory
53
- TERMUX_SERVICE_DIR: path.join(process.env.PREFIX || '/data/data/com.termux/files/usr', 'var', 'service', 'nexuscrew'),
54
-
55
- // Termux runit logs
56
- TERMUX_SERVICE_LOG_DIR: path.join(process.env.PREFIX || '/data/data/com.termux/files/usr', 'var', 'log', 'sv', 'nexuscrew'),
57
-
58
- // Claude CLI history (read-only)
59
- CLAUDE_HISTORY: path.join(HOME, '.claude', 'history.jsonl'),
60
- CLAUDE_PROJECTS: path.join(HOME, '.claude', 'projects'),
61
-
62
- // Default workspace
63
- DEFAULT_WORKSPACE: path.join(HOME, 'Dev')
64
- };
65
-
66
- /**
67
- * Ensure all required directories exist
68
- */
69
- function ensureDirectories() {
70
- const dirs = [
71
- PATHS.CONFIG_DIR,
72
- PATHS.DATA_DIR,
73
- PATHS.LOGS_DIR,
74
- PATHS.ENGINES_DIR,
75
- PATHS.ATTACHMENTS_DIR,
76
- PATHS.BIN_DIR
77
- ];
78
-
79
- for (const dir of dirs) {
80
- if (!fs.existsSync(dir)) {
81
- fs.mkdirSync(dir, { recursive: true });
82
- }
83
- }
84
- }
85
-
86
- /**
87
- * Ensure Termux boot directory exists
88
- */
89
- function ensureBootDirectory() {
90
- const bootDir = path.join(HOME, '.termux', 'boot');
91
- if (!fs.existsSync(bootDir)) {
92
- fs.mkdirSync(bootDir, { recursive: true });
93
- }
94
- return bootDir;
95
- }
96
-
97
- /**
98
- * Get path resolving ~ to HOME
99
- */
100
- function resolvePath(p) {
101
- if (p.startsWith('~')) {
102
- return path.join(HOME, p.slice(1));
103
- }
104
- return path.resolve(p);
105
- }
106
-
107
- /**
108
- * Get relative path from HOME (for display)
109
- */
110
- function toDisplayPath(p) {
111
- if (p.startsWith(HOME)) {
112
- return '~' + p.slice(HOME.length);
113
- }
114
- return p;
115
- }
116
-
117
- module.exports = {
118
- PATHS,
119
- HOME,
120
- ensureDirectories,
121
- ensureBootDirectory,
122
- resolvePath,
123
- toDisplayPath
124
- };