@mmmbuto/nexuscli 0.9.7-termux → 0.9.7001-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.
@@ -11,6 +11,7 @@ const { v4: uuidv4 } = require('uuid');
11
11
  const sessionManager = require('../services/session-manager');
12
12
  const contextBridge = require('../services/context-bridge');
13
13
  const { resolveWorkspacePath } = require('../../utils/workspace');
14
+ const { normalizeAttachments } = require('../../utils/attachments');
14
15
 
15
16
  const router = express.Router();
16
17
  const qwenWrapper = new QwenWrapper();
@@ -44,7 +45,7 @@ router.post('/', async (req, res) => {
44
45
  console.log('[Qwen] === NEW QWEN REQUEST ===');
45
46
  console.log('[Qwen] Body:', JSON.stringify(req.body, null, 2));
46
47
 
47
- const { conversationId, message, model = 'coder-model', workspace } = req.body;
48
+ const { conversationId, message, model = 'coder-model', workspace, attachments } = req.body;
48
49
 
49
50
  if (!message) {
50
51
  return res.status(400).json({ error: 'message required' });
@@ -63,6 +64,13 @@ router.post('/', async (req, res) => {
63
64
  console.warn(`[Qwen] Workspace corrected: ${workspace} → ${workspacePath}`);
64
65
  }
65
66
 
67
+ const attachmentInfo = normalizeAttachments({
68
+ message,
69
+ attachments,
70
+ workspacePath
71
+ });
72
+ const userMessage = attachmentInfo.promptMessage || message;
73
+
66
74
  const frontendConversationId = conversationId || uuidv4();
67
75
  ensureConversation(frontendConversationId, workspacePath);
68
76
 
@@ -91,7 +99,7 @@ router.post('/', async (req, res) => {
91
99
  const lastEngine = Message.getLastEngine(frontendConversationId);
92
100
  const isEngineBridge = lastEngine && lastEngine !== 'qwen';
93
101
 
94
- let promptToSend = message;
102
+ let promptToSend = userMessage;
95
103
 
96
104
  if (isEngineBridge) {
97
105
  const contextResult = await contextBridge.buildContext({
@@ -99,7 +107,7 @@ router.post('/', async (req, res) => {
99
107
  sessionId,
100
108
  fromEngine: lastEngine,
101
109
  toEngine: 'qwen',
102
- userMessage: message
110
+ userMessage: userMessage
103
111
  });
104
112
  promptToSend = contextResult.prompt;
105
113
 
@@ -113,6 +121,15 @@ router.post('/', async (req, res) => {
113
121
  console.log(`[Qwen] Native resume: qwen --resume ${nativeSessionId}`);
114
122
  }
115
123
 
124
+ const attachmentRefs = attachmentInfo.attachmentPaths.map((filePath) => {
125
+ const escapedPath = filePath.replace(/([\\\s,;!?()[\]{}])/g, '\\$1');
126
+ return `@${escapedPath}`;
127
+ });
128
+ if (attachmentRefs.length > 0) {
129
+ promptToSend = `${attachmentRefs.join('\n')}\n\n${promptToSend}`;
130
+ console.log(`[Qwen] Attached ${attachmentRefs.length} file(s) to prompt`);
131
+ }
132
+
116
133
  // Save user message
117
134
  try {
118
135
  Message.create(
@@ -138,6 +155,7 @@ router.post('/', async (req, res) => {
138
155
  threadId: nativeSessionId,
139
156
  model,
140
157
  workspacePath,
158
+ includeDirectories: attachmentInfo.includeDirectories,
141
159
  processId: sessionId,
142
160
  onStatus: (event) => {
143
161
  res.write(`data: ${JSON.stringify(event)}\n\n`);
@@ -83,6 +83,7 @@ class QwenWrapper extends BaseCliWrapper {
83
83
  threadId,
84
84
  model = DEFAULT_MODEL,
85
85
  workspacePath,
86
+ includeDirectories = [],
86
87
  onStatus,
87
88
  processId: processIdOverride
88
89
  }) {
@@ -99,6 +100,14 @@ class QwenWrapper extends BaseCliWrapper {
99
100
  '--include-partial-messages',
100
101
  ];
101
102
 
103
+ if (includeDirectories && includeDirectories.length > 0) {
104
+ includeDirectories.forEach((dir) => {
105
+ if (dir) {
106
+ args.push('--include-directories', dir);
107
+ }
108
+ });
109
+ }
110
+
102
111
  if (threadId) {
103
112
  args.push('--resume', threadId);
104
113
  }
@@ -109,6 +118,9 @@ class QwenWrapper extends BaseCliWrapper {
109
118
  console.log(`[QwenWrapper] ThreadId: ${threadId || '(new session)'}`);
110
119
  console.log(`[QwenWrapper] CWD: ${cwd}`);
111
120
  console.log(`[QwenWrapper] Prompt length: ${prompt.length}`);
121
+ if (includeDirectories && includeDirectories.length > 0) {
122
+ console.log(`[QwenWrapper] Include dirs: ${includeDirectories.join(', ')}`);
123
+ }
112
124
 
113
125
  let ptyProcess;
114
126
  try {
@@ -0,0 +1,100 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { fixTermuxPath } = require('./workspace');
4
+
5
+ const ATTACHMENT_LINE_REGEX = /^\s*\[Attached:\s*(.+?)\s*\]\s*$/;
6
+ const IMAGE_EXTS = new Set([
7
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff', '.tif', '.svg', '.heic'
8
+ ]);
9
+
10
+ function parseAttachmentMarkers(message = '') {
11
+ if (!message) {
12
+ return { cleanMessage: message, paths: [] };
13
+ }
14
+
15
+ const lines = message.split('\n');
16
+ const paths = [];
17
+ const kept = [];
18
+
19
+ for (const line of lines) {
20
+ const match = line.match(ATTACHMENT_LINE_REGEX);
21
+ if (match) {
22
+ paths.push(match[1]);
23
+ continue;
24
+ }
25
+ kept.push(line);
26
+ }
27
+
28
+ const cleanMessage = kept.join('\n').trim();
29
+ return { cleanMessage, paths };
30
+ }
31
+
32
+ function isImageAttachment(attachment) {
33
+ const mimeType = attachment?.mimeType || '';
34
+ if (mimeType.startsWith('image/')) return true;
35
+ const ext = path.extname(attachment?.path || '').toLowerCase();
36
+ return IMAGE_EXTS.has(ext);
37
+ }
38
+
39
+ function normalizeAttachmentPath(rawPath, workspacePath) {
40
+ if (!rawPath || typeof rawPath !== 'string') return null;
41
+ let fixedPath = fixTermuxPath(rawPath);
42
+ if (!path.isAbsolute(fixedPath) && workspacePath) {
43
+ fixedPath = path.resolve(workspacePath, fixedPath);
44
+ }
45
+ return fixedPath;
46
+ }
47
+
48
+ function normalizeAttachments({ message, rawMessage, attachments, workspacePath }) {
49
+ const parsed = parseAttachmentMarkers(message || '');
50
+ const promptMessage = rawMessage != null ? rawMessage : (parsed.cleanMessage || message || '');
51
+
52
+ const incoming = [];
53
+ if (Array.isArray(attachments)) {
54
+ incoming.push(...attachments);
55
+ }
56
+ if (parsed.paths.length > 0) {
57
+ parsed.paths.forEach((p) => incoming.push({ path: p }));
58
+ }
59
+
60
+ const seen = new Set();
61
+ const normalized = [];
62
+
63
+ for (const entry of incoming) {
64
+ if (!entry) continue;
65
+ const rawPath = typeof entry === 'string' ? entry : entry.path;
66
+ const resolvedPath = normalizeAttachmentPath(rawPath, workspacePath);
67
+ if (!resolvedPath) continue;
68
+ if (!fs.existsSync(resolvedPath)) continue;
69
+ if (seen.has(resolvedPath)) continue;
70
+
71
+ seen.add(resolvedPath);
72
+ const record = {
73
+ ...entry,
74
+ path: resolvedPath,
75
+ name: entry.name || path.basename(resolvedPath)
76
+ };
77
+ normalized.push(record);
78
+ }
79
+
80
+ const attachmentPaths = normalized.map((att) => att.path);
81
+ const includeDirectories = Array.from(
82
+ new Set(attachmentPaths.map((p) => path.dirname(p)))
83
+ );
84
+ const imageFiles = normalized.filter(isImageAttachment).map((att) => att.path);
85
+
86
+ return {
87
+ promptMessage,
88
+ attachments: normalized,
89
+ attachmentPaths,
90
+ includeDirectories,
91
+ imageFiles,
92
+ cleanMessage: parsed.cleanMessage
93
+ };
94
+ }
95
+
96
+ module.exports = {
97
+ normalizeAttachments,
98
+ parseAttachmentMarkers,
99
+ isImageAttachment
100
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscli",
3
- "version": "0.9.7-termux",
3
+ "version": "0.9.7001-termux",
4
4
  "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini/Qwen)",
5
5
  "main": "lib/server/server.js",
6
6
  "bin": {