metame-cli 1.4.12 → 1.4.15
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/README.md +9 -9
- package/index.js +205 -57
- package/package.json +2 -2
- package/scripts/daemon-admin-commands.js +365 -0
- package/scripts/daemon-agent-commands.js +491 -0
- package/scripts/daemon-agent-tools.js +256 -0
- package/scripts/daemon-bridges.js +236 -0
- package/scripts/daemon-checkpoints.js +89 -0
- package/scripts/daemon-claude-engine.js +909 -0
- package/scripts/daemon-command-router.js +416 -0
- package/scripts/daemon-default.yaml +2 -2
- package/scripts/daemon-exec-commands.js +290 -0
- package/scripts/daemon-file-browser.js +219 -0
- package/scripts/daemon-notify.js +64 -0
- package/scripts/daemon-ops-commands.js +275 -0
- package/scripts/daemon-runtime-lifecycle.js +133 -0
- package/scripts/daemon-session-commands.js +436 -0
- package/scripts/daemon-session-store.js +423 -0
- package/scripts/daemon-task-scheduler.js +539 -0
- package/scripts/daemon.js +555 -4316
- package/scripts/memory-extract.js +15 -9
- package/scripts/session-analytics.js +116 -0
- package/scripts/test_daemon.js +1407 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createSessionCommandHandler(deps) {
|
|
4
|
+
const {
|
|
5
|
+
fs,
|
|
6
|
+
path,
|
|
7
|
+
HOME,
|
|
8
|
+
log,
|
|
9
|
+
loadConfig,
|
|
10
|
+
loadState,
|
|
11
|
+
saveState,
|
|
12
|
+
normalizeCwd,
|
|
13
|
+
expandPath,
|
|
14
|
+
sendBrowse,
|
|
15
|
+
sendDirPicker,
|
|
16
|
+
createSession,
|
|
17
|
+
getCachedFile,
|
|
18
|
+
getSession,
|
|
19
|
+
listRecentSessions,
|
|
20
|
+
getSessionFileMtime,
|
|
21
|
+
formatRelativeTime,
|
|
22
|
+
sendDirListing,
|
|
23
|
+
writeSessionName,
|
|
24
|
+
getSessionName,
|
|
25
|
+
loadSessionTags,
|
|
26
|
+
sessionRichLabel,
|
|
27
|
+
buildSessionCardElements,
|
|
28
|
+
sessionLabel,
|
|
29
|
+
} = deps;
|
|
30
|
+
|
|
31
|
+
async function handleSessionCommand(ctx) {
|
|
32
|
+
const { bot, chatId, text } = ctx;
|
|
33
|
+
|
|
34
|
+
// --- Browse handler (directory navigation) ---
|
|
35
|
+
if (text.startsWith('/browse ')) {
|
|
36
|
+
const parts = text.slice(8).trim().split(' ');
|
|
37
|
+
const mode = parts[0]; // 'new', 'cd', or 'bind'
|
|
38
|
+
// Last token may be a page number
|
|
39
|
+
const lastPart = parts[parts.length - 1];
|
|
40
|
+
const page = /^\d+$/.test(lastPart) ? parseInt(lastPart, 10) : 0;
|
|
41
|
+
const pathParts = /^\d+$/.test(lastPart) ? parts.slice(1, -1) : parts.slice(1);
|
|
42
|
+
const dirPath = expandPath(pathParts.join(' '));
|
|
43
|
+
if (mode && dirPath && fs.existsSync(dirPath)) {
|
|
44
|
+
await sendBrowse(bot, chatId, mode, dirPath, null, page);
|
|
45
|
+
} else if (/^p\d+$/.test(dirPath)) {
|
|
46
|
+
await bot.sendMessage(chatId, '⚠️ Button expired. Pick again:');
|
|
47
|
+
await sendDirPicker(bot, chatId, mode || 'cd', 'Switch workdir:');
|
|
48
|
+
} else {
|
|
49
|
+
await bot.sendMessage(chatId, 'Invalid browse path.');
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (text === '/new' || text.startsWith('/new ')) {
|
|
55
|
+
const arg = text.slice(4).trim();
|
|
56
|
+
if (!arg) {
|
|
57
|
+
// In a dedicated agent group, use the agent's bound cwd directly
|
|
58
|
+
const newCfg = loadConfig();
|
|
59
|
+
const agentMap = { ...(newCfg.telegram ? newCfg.telegram.chat_agent_map : {}), ...(newCfg.feishu ? newCfg.feishu.chat_agent_map : {}) };
|
|
60
|
+
const boundKey = agentMap[String(chatId)];
|
|
61
|
+
const boundProj = boundKey && newCfg.projects && newCfg.projects[boundKey];
|
|
62
|
+
if (boundProj && boundProj.cwd) {
|
|
63
|
+
const boundCwd = normalizeCwd(boundProj.cwd);
|
|
64
|
+
const session = createSession(chatId, boundCwd, '');
|
|
65
|
+
await bot.sendMessage(chatId, `✅ 新会话已创建\nWorkdir: ${session.cwd}`);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
// Non-dedicated group: show directory picker
|
|
69
|
+
await sendDirPicker(bot, chatId, 'new', 'Pick a workdir:');
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
// Parse: /new <path> [name] — if arg contains a space after a valid path, rest is name
|
|
73
|
+
let dirPath = expandPath(arg);
|
|
74
|
+
let sessionName = '';
|
|
75
|
+
// Try full arg as path first; if not, split on spaces to find path + name
|
|
76
|
+
if (!fs.existsSync(dirPath)) {
|
|
77
|
+
const spaceIdx = arg.indexOf(' ');
|
|
78
|
+
if (spaceIdx > 0) {
|
|
79
|
+
const maybePath = arg.slice(0, spaceIdx);
|
|
80
|
+
if (fs.existsSync(maybePath)) {
|
|
81
|
+
dirPath = maybePath;
|
|
82
|
+
sessionName = arg.slice(spaceIdx + 1).trim();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!fs.existsSync(dirPath)) {
|
|
86
|
+
await bot.sendMessage(chatId, `Path not found: ${dirPath}`);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const session = createSession(chatId, dirPath, sessionName || '');
|
|
91
|
+
const label = sessionName ? `[${sessionName}]` : '';
|
|
92
|
+
await bot.sendMessage(chatId, `New session ${label}\nWorkdir: ${session.cwd}`);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// /file <shortId> — send cached file (from button callback)
|
|
97
|
+
if (text.startsWith('/file ')) {
|
|
98
|
+
const shortId = text.slice(6).trim();
|
|
99
|
+
const filePath = getCachedFile(shortId);
|
|
100
|
+
if (!filePath) {
|
|
101
|
+
await bot.sendMessage(chatId, '⏰ 文件链接已过期,请重新生成');
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (!fs.existsSync(filePath)) {
|
|
105
|
+
await bot.sendMessage(chatId, '❌ 文件不存在');
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
if (bot.sendFile) {
|
|
109
|
+
try {
|
|
110
|
+
// Insert zero-width space before extension to prevent link parsing
|
|
111
|
+
const basename = path.basename(filePath);
|
|
112
|
+
const dotIdx = basename.lastIndexOf('.');
|
|
113
|
+
const safeBasename = dotIdx > 0 ? basename.slice(0, dotIdx) + '\u200B' + basename.slice(dotIdx) : basename;
|
|
114
|
+
await bot.sendMessage(chatId, `⏳ 正在发送「${safeBasename}」...`);
|
|
115
|
+
await bot.sendFile(chatId, filePath);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
log('ERROR', `File send failed: ${e.message}`);
|
|
118
|
+
await bot.sendMessage(chatId, `❌ 发送失败: ${e.message.slice(0, 100)}`);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
await bot.sendMessage(chatId, '❌ 当前平台不支持文件发送');
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// /last — smart resume: prefer current cwd, then most recent globally
|
|
127
|
+
if (text === '/last') {
|
|
128
|
+
const curSession = getSession(chatId);
|
|
129
|
+
const curCwd = curSession ? curSession.cwd : null;
|
|
130
|
+
|
|
131
|
+
// Strategy: try current cwd first, then fall back to global
|
|
132
|
+
let s = null;
|
|
133
|
+
if (curCwd) {
|
|
134
|
+
const cwdSessions = listRecentSessions(1, curCwd);
|
|
135
|
+
if (cwdSessions.length > 0) s = cwdSessions[0];
|
|
136
|
+
}
|
|
137
|
+
if (!s) {
|
|
138
|
+
const globalSessions = listRecentSessions(1);
|
|
139
|
+
if (globalSessions.length > 0) s = globalSessions[0];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!s) {
|
|
143
|
+
// Last resort: use __continue__ to resume whatever Claude thinks is last
|
|
144
|
+
const state2 = loadState();
|
|
145
|
+
state2.sessions[chatId] = {
|
|
146
|
+
id: '__continue__',
|
|
147
|
+
cwd: curCwd || HOME,
|
|
148
|
+
created: new Date().toISOString(),
|
|
149
|
+
started: true,
|
|
150
|
+
};
|
|
151
|
+
saveState(state2);
|
|
152
|
+
await bot.sendMessage(chatId, `⚡ Resuming last session in ${path.basename(curCwd || HOME)}`);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const state2 = loadState();
|
|
157
|
+
state2.sessions[chatId] = {
|
|
158
|
+
id: s.sessionId,
|
|
159
|
+
cwd: s.projectPath || HOME,
|
|
160
|
+
started: true,
|
|
161
|
+
};
|
|
162
|
+
saveState(state2);
|
|
163
|
+
// Display: name/summary + id on separate lines
|
|
164
|
+
const name = s.customTitle;
|
|
165
|
+
const shortId = s.sessionId.slice(0, 8);
|
|
166
|
+
const title = name ? `[${name}]` : (s.summary || s.firstPrompt || '').slice(0, 40) || 'Session';
|
|
167
|
+
// Get real file mtime for accuracy
|
|
168
|
+
const realMtime = getSessionFileMtime(s.sessionId, s.projectPath);
|
|
169
|
+
const ago = formatRelativeTime(new Date(realMtime || s.fileMtime || new Date(s.modified).getTime()).toISOString());
|
|
170
|
+
await bot.sendMessage(chatId, `⚡ ${title}\n📁 ${path.basename(s.projectPath || '')} #${shortId}\n🕐 ${ago}`);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// /memory [keyword] — show memory stats or search facts
|
|
175
|
+
if (text === '/memory' || text.startsWith('/memory ')) {
|
|
176
|
+
const query = text.startsWith('/memory ') ? text.slice(8).trim() : '';
|
|
177
|
+
let memMod;
|
|
178
|
+
try { memMod = require('./memory'); } catch { await bot.sendMessage(chatId, '❌ Memory module not available'); return true; }
|
|
179
|
+
|
|
180
|
+
if (!query) {
|
|
181
|
+
// Stats view
|
|
182
|
+
try {
|
|
183
|
+
const s = memMod.stats();
|
|
184
|
+
const factCount = s.facts ?? '?';
|
|
185
|
+
const tagFile = path.join(HOME, '.metame', 'session_tags.json');
|
|
186
|
+
let tagCount = 0;
|
|
187
|
+
try { tagCount = Object.keys(JSON.parse(fs.readFileSync(tagFile, 'utf8'))).length; } catch { }
|
|
188
|
+
const lines = [
|
|
189
|
+
'🧠 *Memory Stats*',
|
|
190
|
+
'━━━━━━━━━━━━━━━━',
|
|
191
|
+
`📌 Facts: ${factCount}`,
|
|
192
|
+
`🏷 Sessions tagged: ${tagCount}`,
|
|
193
|
+
`🗃 Sessions in DB: ${s.count}`,
|
|
194
|
+
`💾 DB size: ${s.dbSizeKB} KB`,
|
|
195
|
+
s.newestDate ? `🕐 Last updated: ${new Date(s.newestDate).toLocaleDateString()}` : '',
|
|
196
|
+
'',
|
|
197
|
+
'搜索: /memory <关键词>',
|
|
198
|
+
].filter(l => l !== undefined && !(l === '' && false));
|
|
199
|
+
await bot.sendMessage(chatId, lines.join('\n'));
|
|
200
|
+
} catch (e) {
|
|
201
|
+
await bot.sendMessage(chatId, `❌ Memory stats error: ${e.message}`);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
// Search facts
|
|
205
|
+
try {
|
|
206
|
+
const results = await memMod.searchFactsAsync(query, { limit: 5 });
|
|
207
|
+
if (!results || results.length === 0) {
|
|
208
|
+
await bot.sendMessage(chatId, `🔍 No facts found for「${query}」`);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
let msg = `🔍 *Facts: "${query}"* (${results.length})\n━━━━━━━━━━━━━━━━\n`;
|
|
212
|
+
for (const r of results) {
|
|
213
|
+
const tag = r.confidence === 'high' ? '🟢' : '🟡';
|
|
214
|
+
msg += `${tag} *${r.entity}*\n${r.value}\n\n`;
|
|
215
|
+
}
|
|
216
|
+
await bot.sendMessage(chatId, msg.trim());
|
|
217
|
+
} catch (e) {
|
|
218
|
+
await bot.sendMessage(chatId, `❌ Search error: ${e.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// /sessions — compact list, tap to see details, then tap to switch
|
|
225
|
+
if (text === '/sessions') {
|
|
226
|
+
const allSessions = listRecentSessions(15);
|
|
227
|
+
if (allSessions.length === 0) {
|
|
228
|
+
await bot.sendMessage(chatId, 'No sessions found. Try /new first.');
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
if (bot.sendButtons) {
|
|
232
|
+
await bot.sendRawCard(chatId, '📋 Recent Sessions', buildSessionCardElements(allSessions));
|
|
233
|
+
} else {
|
|
234
|
+
const _tags1 = loadSessionTags();
|
|
235
|
+
let msg = '📋 Recent sessions:\n\n';
|
|
236
|
+
allSessions.forEach((s, i) => {
|
|
237
|
+
msg += sessionRichLabel(s, i + 1, _tags1) + '\n';
|
|
238
|
+
});
|
|
239
|
+
await bot.sendMessage(chatId, msg);
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// /sess <id> — show session detail card with switch button
|
|
245
|
+
if (text.startsWith('/sess ')) {
|
|
246
|
+
const sid = text.slice(6).trim();
|
|
247
|
+
const allSessions = listRecentSessions(50);
|
|
248
|
+
const s = allSessions.find(x => x.sessionId === sid || x.sessionId.startsWith(sid));
|
|
249
|
+
if (!s) {
|
|
250
|
+
await bot.sendMessage(chatId, `Session not found: ${sid.slice(0, 8)}`);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
const proj = s.projectPath || '~';
|
|
254
|
+
const projName = path.basename(proj);
|
|
255
|
+
const realMtime = getSessionFileMtime(s.sessionId, s.projectPath);
|
|
256
|
+
const timeMs = realMtime || s.fileMtime || new Date(s.modified).getTime();
|
|
257
|
+
const ago = formatRelativeTime(new Date(timeMs).toISOString());
|
|
258
|
+
const sessionTags = loadSessionTags();
|
|
259
|
+
const tagEntry = sessionTags[s.sessionId] || {};
|
|
260
|
+
const tagName = tagEntry.name || '';
|
|
261
|
+
const tags = (tagEntry.tags || []).slice(0, 5);
|
|
262
|
+
const title = s.customTitle || tagName || '';
|
|
263
|
+
const summary = s.summary || '';
|
|
264
|
+
const firstMsg = (s.firstPrompt || '').replace(/^<[^>]+>.*?<\/[^>]+>\s*/s, '');
|
|
265
|
+
const msgs = s.messageCount || '?';
|
|
266
|
+
|
|
267
|
+
let detail = '📋 Session Detail\n';
|
|
268
|
+
detail += '━━━━━━━━━━━━━━━━━━━━\n';
|
|
269
|
+
if (title) detail += `📝 Title: ${title}\n`;
|
|
270
|
+
if (tags.length) detail += `🏷 Tags: ${tags.map(t => '#' + t).join(' ')}\n`;
|
|
271
|
+
if (summary) detail += `💡 Summary: ${summary}\n`;
|
|
272
|
+
detail += `📁 Project: ${projName}\n`;
|
|
273
|
+
detail += `📂 Path: ${proj}\n`;
|
|
274
|
+
detail += `💬 Messages: ${msgs}\n`;
|
|
275
|
+
detail += `🕐 Last active: ${ago}\n`;
|
|
276
|
+
detail += `🆔 ID: ${s.sessionId.slice(0, 8)}`;
|
|
277
|
+
if (firstMsg && firstMsg !== summary) detail += `\n\n🗨️ First message:\n${firstMsg}`;
|
|
278
|
+
|
|
279
|
+
if (bot.sendCard) {
|
|
280
|
+
// Build rich detail as markdown body + buttons
|
|
281
|
+
let body = '';
|
|
282
|
+
if (title) body += `**📝 ${title}**\n`;
|
|
283
|
+
if (tags.length) body += `${tags.map(t => `\`${t}\``).join(' ')}\n`;
|
|
284
|
+
if (summary) body += `💡 ${summary}\n`;
|
|
285
|
+
body += `📁 ${projName} · 📂 ${proj}\n`;
|
|
286
|
+
body += `💬 ${msgs} messages · 🕐 ${ago}\n`;
|
|
287
|
+
body += `🆔 ${s.sessionId.slice(0, 8)}`;
|
|
288
|
+
if (firstMsg && firstMsg !== summary) body += `\n\n🗨️ ${firstMsg.slice(0, 100)}`;
|
|
289
|
+
const elements = [
|
|
290
|
+
{ tag: 'div', text: { tag: 'lark_md', content: body } },
|
|
291
|
+
{ tag: 'hr' },
|
|
292
|
+
{
|
|
293
|
+
tag: 'action', actions: [
|
|
294
|
+
{ tag: 'button', text: { tag: 'plain_text', content: '▶️ Switch to this session' }, type: 'primary', value: { cmd: `/resume ${s.sessionId}` } },
|
|
295
|
+
{ tag: 'button', text: { tag: 'plain_text', content: '⬅️ Back to list' }, type: 'default', value: { cmd: '/sessions' } },
|
|
296
|
+
]
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
await bot.sendRawCard(chatId, '📋 Session Detail', elements);
|
|
300
|
+
} else if (bot.sendButtons) {
|
|
301
|
+
await bot.sendButtons(chatId, detail, [
|
|
302
|
+
[{ text: '▶️ Switch to this session', callback_data: `/resume ${s.sessionId}` }],
|
|
303
|
+
[{ text: '⬅️ Back to list', callback_data: '/sessions' }],
|
|
304
|
+
]);
|
|
305
|
+
} else {
|
|
306
|
+
await bot.sendMessage(chatId, detail + `\n\n/resume ${s.sessionId.slice(0, 8)}`);
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (text === '/cd' || text.startsWith('/cd ')) {
|
|
312
|
+
let newCwd = expandPath(text.slice(3).trim());
|
|
313
|
+
if (!newCwd) {
|
|
314
|
+
await sendDirPicker(bot, chatId, 'cd', 'Switch workdir:');
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
// /cd last — sync to computer: switch to most recent session AND its directory
|
|
318
|
+
if (newCwd === 'last') {
|
|
319
|
+
const currentSession = getSession(chatId);
|
|
320
|
+
const excludeId = currentSession?.id;
|
|
321
|
+
const recent = listRecentSessions(10);
|
|
322
|
+
const filtered = excludeId ? recent.filter(s => s.sessionId !== excludeId) : recent;
|
|
323
|
+
if (filtered.length > 0 && filtered[0].projectPath) {
|
|
324
|
+
const target = filtered[0];
|
|
325
|
+
// Switch to that session (like /resume) AND its directory
|
|
326
|
+
const state2 = loadState();
|
|
327
|
+
state2.sessions[chatId] = {
|
|
328
|
+
id: target.sessionId,
|
|
329
|
+
cwd: target.projectPath,
|
|
330
|
+
started: true,
|
|
331
|
+
};
|
|
332
|
+
saveState(state2);
|
|
333
|
+
const name = target.customTitle || target.summary || '';
|
|
334
|
+
const label = name ? name.slice(0, 40) : target.sessionId.slice(0, 8);
|
|
335
|
+
await bot.sendMessage(chatId, `🔄 Synced to: ${label}\n📁 ${path.basename(target.projectPath)}`);
|
|
336
|
+
await sendDirListing(bot, chatId, target.projectPath, null);
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
await bot.sendMessage(chatId, 'No recent session found.');
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
if (!fs.existsSync(newCwd)) {
|
|
343
|
+
// Likely an expired path shortcode (e.g. p16) from a daemon restart
|
|
344
|
+
if (/^p\d+$/.test(newCwd)) {
|
|
345
|
+
await bot.sendMessage(chatId, '⚠️ Button expired (daemon restarted). Pick again:');
|
|
346
|
+
await sendDirPicker(bot, chatId, 'cd', 'Switch workdir:');
|
|
347
|
+
} else {
|
|
348
|
+
await bot.sendMessage(chatId, `Path not found: ${newCwd}`);
|
|
349
|
+
}
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
const state2 = loadState();
|
|
353
|
+
// Try to find existing session in this directory
|
|
354
|
+
const recentInDir = listRecentSessions(1, newCwd);
|
|
355
|
+
if (recentInDir.length > 0 && recentInDir[0].sessionId) {
|
|
356
|
+
// Attach to existing session in this directory
|
|
357
|
+
const target = recentInDir[0];
|
|
358
|
+
state2.sessions[chatId] = {
|
|
359
|
+
id: target.sessionId,
|
|
360
|
+
cwd: newCwd,
|
|
361
|
+
started: true,
|
|
362
|
+
};
|
|
363
|
+
saveState(state2);
|
|
364
|
+
const label = target.customTitle || target.summary?.slice(0, 30) || target.sessionId.slice(0, 8);
|
|
365
|
+
await bot.sendMessage(chatId, `📁 ${path.basename(newCwd)}\n🔄 Attached: ${label}`);
|
|
366
|
+
} else if (!state2.sessions[chatId]) {
|
|
367
|
+
createSession(chatId, newCwd);
|
|
368
|
+
await bot.sendMessage(chatId, `📁 ${path.basename(newCwd)} (new session)`);
|
|
369
|
+
} else {
|
|
370
|
+
state2.sessions[chatId].cwd = newCwd;
|
|
371
|
+
saveState(state2);
|
|
372
|
+
await bot.sendMessage(chatId, `📁 ${path.basename(newCwd)}`);
|
|
373
|
+
}
|
|
374
|
+
await sendDirListing(bot, chatId, newCwd, null);
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// /list [subdir|glob|fullpath] — list files (zero token, daemon-only)
|
|
379
|
+
if (text === '/list' || text.startsWith('/list ')) {
|
|
380
|
+
const session = getSession(chatId);
|
|
381
|
+
const cwd = session?.cwd || HOME;
|
|
382
|
+
const arg = text.slice(5).trim();
|
|
383
|
+
// If arg is an absolute or ~ path, list that directly
|
|
384
|
+
const expanded = arg ? expandPath(arg) : null;
|
|
385
|
+
if (expanded && /^p\d+$/.test(expanded)) {
|
|
386
|
+
// Expired shortcode from daemon restart
|
|
387
|
+
await bot.sendMessage(chatId, '⚠️ Button expired. Refreshing...');
|
|
388
|
+
await sendDirListing(bot, chatId, cwd, null);
|
|
389
|
+
} else if (expanded && path.isAbsolute(expanded) && fs.existsSync(expanded) && fs.statSync(expanded).isDirectory()) {
|
|
390
|
+
await sendDirListing(bot, chatId, expanded, null);
|
|
391
|
+
} else {
|
|
392
|
+
await sendDirListing(bot, chatId, cwd, arg || null);
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (text.startsWith('/name ')) {
|
|
398
|
+
const name = text.slice(6).trim();
|
|
399
|
+
if (!name) {
|
|
400
|
+
await bot.sendMessage(chatId, 'Usage: /name <session name>');
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
const session = getSession(chatId);
|
|
404
|
+
if (!session) {
|
|
405
|
+
await bot.sendMessage(chatId, 'No active session. Start one first.');
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Write to Claude's session file (unified with /rename on desktop)
|
|
410
|
+
if (writeSessionName(session.id, session.cwd, name)) {
|
|
411
|
+
await bot.sendMessage(chatId, `✅ Session: [${name}]`);
|
|
412
|
+
} else {
|
|
413
|
+
await bot.sendMessage(chatId, '⚠️ Failed to save name, but session continues.');
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (text === '/session') {
|
|
419
|
+
const session = getSession(chatId);
|
|
420
|
+
if (!session) {
|
|
421
|
+
await bot.sendMessage(chatId, 'No active session. Send any message to start one.');
|
|
422
|
+
} else {
|
|
423
|
+
const name = getSessionName(session.id);
|
|
424
|
+
const nameTag = name ? ` [${name}]` : '';
|
|
425
|
+
await bot.sendMessage(chatId, `Session: ${session.id.slice(0, 8)}...${nameTag}\nWorkdir: ${session.cwd}`);
|
|
426
|
+
}
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return { handleSessionCommand };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
module.exports = { createSessionCommandHandler };
|