chatlab-cli 0.29.0 → 0.30.1-beta.1
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/dist/{chat-command-JKG3FY5B.mjs → chat-command-VBDMWOKL.mjs} +8 -8
- package/dist/{chunk-6FPEP2ZX.mjs → chunk-326OCJOT.mjs} +2 -2
- package/dist/{chunk-QQK7MLCR.mjs → chunk-36JGKW6K.mjs} +710 -282
- package/dist/chunk-36JGKW6K.mjs.map +1 -0
- package/dist/{chunk-SKUG5XVD.mjs → chunk-5EE2EFPE.mjs} +5 -4
- package/dist/{chunk-KL2NGIMQ.mjs → chunk-D3DPCDII.mjs} +444 -274
- package/dist/chunk-D3DPCDII.mjs.map +1 -0
- package/dist/{chunk-I35WHWA6.mjs → chunk-KBSAVUFV.mjs} +7 -4
- package/dist/chunk-KBSAVUFV.mjs.map +1 -0
- package/dist/{chunk-WPI6YSAR.mjs → chunk-LVEJHB3X.mjs} +4570 -4412
- package/dist/chunk-LVEJHB3X.mjs.map +1 -0
- package/dist/chunk-M2PXDX4G.mjs +2325 -0
- package/dist/chunk-M2PXDX4G.mjs.map +1 -0
- package/dist/{chunk-CIB23U32.mjs → chunk-TCZHG3G7.mjs} +101 -41
- package/dist/chunk-TCZHG3G7.mjs.map +1 -0
- package/dist/{chunk-D4WTE7LV.mjs → chunk-TEX44HNL.mjs} +16 -12
- package/dist/chunk-TEX44HNL.mjs.map +1 -0
- package/dist/{chunk-7RW4CQAO.mjs → chunk-ULNRUS55.mjs} +5 -3
- package/dist/chunk-ULNRUS55.mjs.map +1 -0
- package/dist/{chunk-2MZ6G4AC.mjs → chunk-WV5GSRQM.mjs} +2 -2
- package/dist/cli.mjs +7 -7
- package/dist/contacts-worker.mjs +2 -2
- package/dist/{http-5E7ARYFW.mjs → http-L4AMELPZ.mjs} +11 -11
- package/dist/{import-3KG6VK6O.mjs → import-WWCSXGVS.mjs} +7 -7
- package/dist/index.mjs +11 -11
- package/dist/{mcp-JBI4CUQT.mjs → mcp-A4M3LT5O.mjs} +8 -8
- package/dist/people-relationships-worker.mjs +2 -2
- package/dist/semantic-index-worker.mjs +1 -1
- package/dist/{src-UTBQAOJ3.mjs → src-MPNBTFOE.mjs} +14 -6
- package/dist/{stream-import-7HZ4IRYJ.mjs → stream-import-HIE7PNL4.mjs} +7 -7
- package/dist/{worker-runtime-ALS4MR2A.mjs → worker-runtime-BJXO24XX.mjs} +5 -5
- package/dist-web/assets/{BufferResource-MsqP5sq3.js → BufferResource-DPkMC1wo.js} +1 -1
- package/dist-web/assets/CanvasRenderer-DokMtSX9.js +1 -0
- package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-Ceg-0WOD.js +110 -0
- package/dist-web/assets/{Filter-wRJtdXx9.js → Filter-Bo_p3BVX.js} +1 -1
- package/dist-web/assets/LoadingState.vue_vue_type_script_setup_true_lang-D_dJuLWJ.js +1 -0
- package/dist-web/assets/{MessageExportModal-BCrMf5Kw.js → MessageExportModal-Ch0tqQix.js} +1 -1
- package/dist-web/assets/{PageHeader-C_yw54le.js → PageHeader-CRlmZoKw.js} +1 -1
- package/dist-web/assets/{RelationshipGalaxyCanvas-DTzJq7q7.js → RelationshipGalaxyCanvas-W_6XfPOG.js} +3 -3
- package/dist-web/assets/{RelationshipGalaxyThreeCanvas-DIE-0_iM.js → RelationshipGalaxyThreeCanvas-bB1nCwQ_.js} +1 -1
- package/dist-web/assets/{RenderTargetSystem-BSAQFamI.js → RenderTargetSystem-obRi6fMT.js} +1 -1
- package/dist-web/assets/{WebGLRenderer-lfzmCZNT.js → WebGLRenderer-xcDlnrCZ.js} +1 -1
- package/dist-web/assets/{WebGPURenderer-BnEdfdr_.js → WebGPURenderer-DbAOiiYr.js} +1 -1
- package/dist-web/assets/{browserAll-gRNfDyPu.js → browserAll-DVRJimO8.js} +1 -1
- package/dist-web/assets/electron-BSGRuxX9.js +1 -0
- package/dist-web/assets/{fetch-BW3aO7PS.js → fetch-7UN4YW3A.js} +1 -1
- package/dist-web/assets/{fetch-CQt-gb-f.js → fetch-B2alAySK.js} +1 -1
- package/dist-web/assets/fetch-B2rVTMYY.js +17 -0
- package/dist-web/assets/{fetch-C1fI_KOX.js → fetch-BSShKcMt.js} +1 -1
- package/dist-web/assets/{fetch-mFlEBZyB.js → fetch-BmxMf3I6.js} +1 -1
- package/dist-web/assets/{fetch-BwF5hNuF.js → fetch-CR5UDFW7.js} +1 -1
- package/dist-web/assets/{fetch-CFRoSIAn.js → fetch-ChAsglT5.js} +1 -1
- package/dist-web/assets/{fetch-CN91TIwV.js → fetch-DWEjTdcP.js} +1 -1
- package/dist-web/assets/{fetch-DPCJ-1l-.js → fetch-DfoxedXR.js} +1 -1
- package/dist-web/assets/{fetch-gMB4wuBd.js → fetch-t0WsRKHW.js} +1 -1
- package/dist-web/assets/index-B4l3rHie.css +1 -0
- package/dist-web/assets/{index-ZdCB7xcf.js → index-BVUTV9_w.js} +4 -4
- package/dist-web/assets/index-BmXNdMV5.js +2 -0
- package/dist-web/assets/index-BsXljrlg.js +22 -0
- package/dist-web/assets/{index-MBRD3wPd.css → index-C1p9CbmN.css} +1 -1
- package/dist-web/assets/index-COpza1A4.css +1 -0
- package/dist-web/assets/index-CQ5lRCHt.js +173 -0
- package/dist-web/assets/index-CZv0ZOID.js +2 -0
- package/dist-web/assets/{index-DveHdPfK.js → index-CfgDqd_z.js} +1 -1
- package/dist-web/assets/{index-Bg4GGPXv.js → index-CtVR_KuP.js} +1 -1
- package/dist-web/assets/index-DT0Bu8mH.js +64 -0
- package/dist-web/assets/index-DjcWJv4b.js +1 -0
- package/dist-web/assets/{vendor-nuxt-ui-Dr2T8_OZ.js → vendor-nuxt-ui-DaLxYaC4.js} +1 -1
- package/dist-web/assets/{web-BakeV58u.js → web-knHSvvNj.js} +1 -1
- package/dist-web/assets/{webworkerAll-D_6PgURS.js → webworkerAll-CMKoykfD.js} +1 -1
- package/dist-web/index.html +3 -3
- package/package.json +1 -1
- package/dist/chunk-7RW4CQAO.mjs.map +0 -1
- package/dist/chunk-ARZM6HBX.mjs +0 -542
- package/dist/chunk-ARZM6HBX.mjs.map +0 -1
- package/dist/chunk-CIB23U32.mjs.map +0 -1
- package/dist/chunk-D4WTE7LV.mjs.map +0 -1
- package/dist/chunk-I35WHWA6.mjs.map +0 -1
- package/dist/chunk-KL2NGIMQ.mjs.map +0 -1
- package/dist/chunk-QQK7MLCR.mjs.map +0 -1
- package/dist/chunk-WPI6YSAR.mjs.map +0 -1
- package/dist-web/assets/CanvasRenderer-BKCvcc2p.js +0 -1
- package/dist-web/assets/DebugToolsPanel.vue_vue_type_script_setup_true_lang-HGJb0Et1.js +0 -110
- package/dist-web/assets/electron-BHaEaaZP.js +0 -1
- package/dist-web/assets/fetch-CBZbgZqk.js +0 -17
- package/dist-web/assets/index--6C0sK6a.css +0 -1
- package/dist-web/assets/index-B6wEGata.js +0 -173
- package/dist-web/assets/index-BgVbj5qX.js +0 -12
- package/dist-web/assets/index-BkoXaBV_.js +0 -64
- package/dist-web/assets/index-C56IuV5U.css +0 -1
- package/dist-web/assets/index-CLRaKwpT.js +0 -2
- package/dist-web/assets/index-Dj1BSD7O.js +0 -1
- package/dist-web/assets/index-RoDgJcHU.js +0 -2
- package/dist-web/assets/index-aljYO5ms.js +0 -1
- /package/dist/{chat-command-JKG3FY5B.mjs.map → chat-command-VBDMWOKL.mjs.map} +0 -0
- /package/dist/{chunk-6FPEP2ZX.mjs.map → chunk-326OCJOT.mjs.map} +0 -0
- /package/dist/{chunk-SKUG5XVD.mjs.map → chunk-5EE2EFPE.mjs.map} +0 -0
- /package/dist/{chunk-2MZ6G4AC.mjs.map → chunk-WV5GSRQM.mjs.map} +0 -0
- /package/dist/{http-5E7ARYFW.mjs.map → http-L4AMELPZ.mjs.map} +0 -0
- /package/dist/{import-3KG6VK6O.mjs.map → import-WWCSXGVS.mjs.map} +0 -0
- /package/dist/{mcp-JBI4CUQT.mjs.map → mcp-A4M3LT5O.mjs.map} +0 -0
- /package/dist/{src-UTBQAOJ3.mjs.map → src-MPNBTFOE.mjs.map} +0 -0
- /package/dist/{stream-import-7HZ4IRYJ.mjs.map → stream-import-HIE7PNL4.mjs.map} +0 -0
- /package/dist/{worker-runtime-ALS4MR2A.mjs.map → worker-runtime-BJXO24XX.mjs.map} +0 -0
|
@@ -0,0 +1,2325 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from 'module';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
4
|
+
const require = __createRequire(import.meta.url);
|
|
5
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = __pathDirname(__filename);
|
|
7
|
+
import {
|
|
8
|
+
assertCliDataDirCompatible
|
|
9
|
+
} from "./chunk-326OCJOT.mjs";
|
|
10
|
+
import {
|
|
11
|
+
getVersion
|
|
12
|
+
} from "./chunk-5ADKYYNA.mjs";
|
|
13
|
+
import {
|
|
14
|
+
resolveCliPath
|
|
15
|
+
} from "./chunk-FJ35V3TE.mjs";
|
|
16
|
+
import {
|
|
17
|
+
NodePathProvider,
|
|
18
|
+
PreferencesManager,
|
|
19
|
+
applyPendingNodeDataDirMigrationIfNeeded,
|
|
20
|
+
getSystemLogsDir,
|
|
21
|
+
hasPendingElectronDataWarning
|
|
22
|
+
} from "./chunk-ULNRUS55.mjs";
|
|
23
|
+
import {
|
|
24
|
+
AIChatManager,
|
|
25
|
+
applyPreprocessingPipeline,
|
|
26
|
+
computeWordFrequency,
|
|
27
|
+
desensitizeText,
|
|
28
|
+
logNativeParserStatus,
|
|
29
|
+
matchesBlacklist,
|
|
30
|
+
mergeRulesForLocale,
|
|
31
|
+
preprocessMessages
|
|
32
|
+
} from "./chunk-36JGKW6K.mjs";
|
|
33
|
+
import {
|
|
34
|
+
ConfigSetError,
|
|
35
|
+
DEFAULT_API_PORT,
|
|
36
|
+
DatabaseManager,
|
|
37
|
+
appLogger,
|
|
38
|
+
computeResponseTimeStats,
|
|
39
|
+
executeParameterizedSql,
|
|
40
|
+
executeReadonlySql,
|
|
41
|
+
getChatOverview,
|
|
42
|
+
getConfigPath,
|
|
43
|
+
getConversationBetween,
|
|
44
|
+
getDailyActivity,
|
|
45
|
+
getDatabaseSchema,
|
|
46
|
+
getHourlyActivity,
|
|
47
|
+
getMemberActivity,
|
|
48
|
+
getMemberNameHistory,
|
|
49
|
+
getMembers,
|
|
50
|
+
getMembersWithAliases,
|
|
51
|
+
getMessageContext,
|
|
52
|
+
getMonthlyActivity,
|
|
53
|
+
getSearchMessageContext,
|
|
54
|
+
getSegmentMessages,
|
|
55
|
+
getSegmentSummaries,
|
|
56
|
+
getSessionMeta,
|
|
57
|
+
getSessionOverview,
|
|
58
|
+
getWeekdayActivity,
|
|
59
|
+
initAppLogger,
|
|
60
|
+
initNlpDir,
|
|
61
|
+
loadConfig,
|
|
62
|
+
messageTypeToString,
|
|
63
|
+
resolveOwnerMember,
|
|
64
|
+
searchMessagesByKeywords,
|
|
65
|
+
searchMessagesLike,
|
|
66
|
+
setConfigField,
|
|
67
|
+
verifyCliDataPath
|
|
68
|
+
} from "./chunk-LVEJHB3X.mjs";
|
|
69
|
+
|
|
70
|
+
// src/cli.ts
|
|
71
|
+
import * as fs2 from "fs";
|
|
72
|
+
import { execSync } from "child_process";
|
|
73
|
+
import { Command } from "commander";
|
|
74
|
+
|
|
75
|
+
// src/http/port.ts
|
|
76
|
+
import * as net from "net";
|
|
77
|
+
function isPortAvailable(port, host) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const server = net.createServer();
|
|
80
|
+
server.once("error", (err) => {
|
|
81
|
+
if (err.code === "EADDRINUSE") {
|
|
82
|
+
resolve(false);
|
|
83
|
+
} else {
|
|
84
|
+
reject(err);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
server.once("listening", () => server.close(() => resolve(true)));
|
|
88
|
+
server.listen(port, host);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function formatPortInUseError(port) {
|
|
92
|
+
return [
|
|
93
|
+
``,
|
|
94
|
+
` \u2716 Error: port ${port} is already in use.`,
|
|
95
|
+
``,
|
|
96
|
+
` Another process (possibly a ChatLab instance) is using this port. You can:`,
|
|
97
|
+
` \u2022 Use another port: chatlab start --port <port>`,
|
|
98
|
+
` \u2022 Find the process: lsof -iTCP:${port} -sTCP:LISTEN`,
|
|
99
|
+
``
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/runtime.ts
|
|
104
|
+
import * as fs from "fs";
|
|
105
|
+
function resolveNativeBinding() {
|
|
106
|
+
if (process.versions.electron) return void 0;
|
|
107
|
+
const nativePath = resolveCliPath("native/better_sqlite3.node");
|
|
108
|
+
if (fs.existsSync(nativePath)) return nativePath;
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
function initRuntime() {
|
|
112
|
+
let config = loadConfig();
|
|
113
|
+
const pendingMigration = applyPendingNodeDataDirMigrationIfNeeded();
|
|
114
|
+
if (!pendingMigration.skipped) {
|
|
115
|
+
if (pendingMigration.success) {
|
|
116
|
+
console.error("[Migration] Pending data directory migration completed");
|
|
117
|
+
config = loadConfig();
|
|
118
|
+
} else {
|
|
119
|
+
console.error("[Migration] Pending data directory migration failed:", pendingMigration.error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const userDataDir = config.data.user_data_dir || void 0;
|
|
123
|
+
const pathProvider = new NodePathProvider(userDataDir);
|
|
124
|
+
pathProvider.ensureAllDirs();
|
|
125
|
+
const runtime = assertCliDataDirCompatible(pathProvider, "cli");
|
|
126
|
+
if (hasPendingElectronDataWarning() || !verifyCliDataPath(pathProvider.getDatabaseDir())) {
|
|
127
|
+
printElectronDataError();
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const nativeBinding = resolveNativeBinding();
|
|
131
|
+
const dbManager = new DatabaseManager(pathProvider, { nativeBinding, runtime });
|
|
132
|
+
return { config, pathProvider, dbManager };
|
|
133
|
+
}
|
|
134
|
+
function printElectronDataError() {
|
|
135
|
+
console.error("\n" + "=".repeat(68));
|
|
136
|
+
console.error(" ChatLab: Electron desktop data not found");
|
|
137
|
+
console.error("=".repeat(68));
|
|
138
|
+
console.error("");
|
|
139
|
+
console.error(" Detected that ChatLab desktop app was installed on this machine,");
|
|
140
|
+
console.error(" but could not locate your chat databases.");
|
|
141
|
+
console.error("");
|
|
142
|
+
console.error(" This usually means you changed the data directory in desktop settings.");
|
|
143
|
+
console.error("");
|
|
144
|
+
console.error(" To fix this, choose one of:");
|
|
145
|
+
console.error("");
|
|
146
|
+
console.error(" 1. Open ChatLab desktop app \u2014 it will auto-migrate your data");
|
|
147
|
+
console.error(" 2. Set the data directory manually:");
|
|
148
|
+
console.error(' export CHATLAB_DATA_DIR="/path/to/your/data"');
|
|
149
|
+
console.error(" 3. Edit ~/.chatlab/config.toml:");
|
|
150
|
+
console.error(" [data]");
|
|
151
|
+
console.error(' user_data_dir = "/path/to/your/data"');
|
|
152
|
+
console.error("");
|
|
153
|
+
console.error("=".repeat(68) + "\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/query/envelope.ts
|
|
157
|
+
var API_VERSION = 1;
|
|
158
|
+
var QueryError = class extends Error {
|
|
159
|
+
code;
|
|
160
|
+
hint;
|
|
161
|
+
candidates;
|
|
162
|
+
constructor(shape) {
|
|
163
|
+
super(shape.message);
|
|
164
|
+
this.name = "QueryError";
|
|
165
|
+
this.code = shape.code;
|
|
166
|
+
this.hint = shape.hint;
|
|
167
|
+
this.candidates = shape.candidates;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
function successEnvelope(command, data, meta) {
|
|
171
|
+
return {
|
|
172
|
+
ok: true,
|
|
173
|
+
command,
|
|
174
|
+
data,
|
|
175
|
+
meta: { ...meta ?? {}, apiVersion: API_VERSION }
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function errorEnvelope(command, error, meta) {
|
|
179
|
+
const shape = {
|
|
180
|
+
code: error.code,
|
|
181
|
+
message: error.message,
|
|
182
|
+
...error.hint !== void 0 ? { hint: error.hint } : {},
|
|
183
|
+
...error.candidates !== void 0 ? { candidates: error.candidates } : {}
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
command,
|
|
188
|
+
error: shape,
|
|
189
|
+
...meta !== void 0 ? { meta } : {}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function exitCodeForError(code) {
|
|
193
|
+
if (code === "SQL_ERROR") return 5;
|
|
194
|
+
if (code.endsWith("_AMBIGUOUS")) return 4;
|
|
195
|
+
if (code.endsWith("_NOT_FOUND")) return 3;
|
|
196
|
+
if (code === "INVALID_ARGUMENT" || code === "CURSOR_INVALID" || code.endsWith("_DISABLED")) return 2;
|
|
197
|
+
return 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/query/runner.ts
|
|
201
|
+
function resolveFormat(explicit) {
|
|
202
|
+
if (explicit !== void 0) {
|
|
203
|
+
if (explicit === "agent" || explicit === "json" || explicit === "text") return explicit;
|
|
204
|
+
throw new QueryError({
|
|
205
|
+
code: "INVALID_ARGUMENT",
|
|
206
|
+
message: `Unknown format: ${explicit}`,
|
|
207
|
+
hint: "Supported formats: agent, json, text"
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return process.stdout.isTTY ? "text" : "agent";
|
|
211
|
+
}
|
|
212
|
+
async function runQuery(command, options, handler) {
|
|
213
|
+
let format;
|
|
214
|
+
try {
|
|
215
|
+
format = resolveFormat(options.format);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
emitError(command, "agent", err);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const result = await handler(format);
|
|
222
|
+
if (format === "text") {
|
|
223
|
+
console.log(result.renderText ? result.renderText() : JSON.stringify(result.data, null, 2));
|
|
224
|
+
} else {
|
|
225
|
+
console.log(JSON.stringify(successEnvelope(command, result.data, result.meta)));
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
emitError(command, format, err);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function emitError(command, format, err) {
|
|
232
|
+
const queryError = err instanceof QueryError ? err : new QueryError({
|
|
233
|
+
code: "INTERNAL_ERROR",
|
|
234
|
+
message: err instanceof Error ? err.message : String(err)
|
|
235
|
+
});
|
|
236
|
+
if (format === "text") {
|
|
237
|
+
console.error(`Error: ${queryError.message}${queryError.hint ? `
|
|
238
|
+
${queryError.hint}` : ""}`);
|
|
239
|
+
if (queryError.candidates) {
|
|
240
|
+
for (const candidate of queryError.candidates) {
|
|
241
|
+
console.error(` - ${JSON.stringify(candidate)}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
console.log(JSON.stringify(errorEnvelope(command, queryError)));
|
|
246
|
+
}
|
|
247
|
+
process.exitCode = exitCodeForError(queryError.code);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/query/context.ts
|
|
251
|
+
import * as path from "path";
|
|
252
|
+
|
|
253
|
+
// src/query/preprocess-config.ts
|
|
254
|
+
var BUILTIN_RULE_LOCALES = ["zh-CN", "en-US", "ja-JP", "ko-KR"];
|
|
255
|
+
var PRIMARY_LANG_MAP = {
|
|
256
|
+
zh: "zh-CN",
|
|
257
|
+
en: "en-US",
|
|
258
|
+
ja: "ja-JP",
|
|
259
|
+
ko: "ko-KR"
|
|
260
|
+
};
|
|
261
|
+
function normalizeLocale(locale) {
|
|
262
|
+
if (BUILTIN_RULE_LOCALES.includes(locale)) return locale;
|
|
263
|
+
const primary = locale.split(/[-_]/)[0].toLowerCase();
|
|
264
|
+
return PRIMARY_LANG_MAP[primary] ?? locale;
|
|
265
|
+
}
|
|
266
|
+
function detectSystemLocale() {
|
|
267
|
+
try {
|
|
268
|
+
const resolved = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
269
|
+
if (resolved) return resolved;
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
const lang = process.env.LANG || "";
|
|
273
|
+
return lang.split(".")[0] || "";
|
|
274
|
+
}
|
|
275
|
+
function resolveCliLocale(configLang, systemLocale) {
|
|
276
|
+
if (configLang) return normalizeLocale(configLang);
|
|
277
|
+
const system = systemLocale !== void 0 ? systemLocale : detectSystemLocale();
|
|
278
|
+
if (system) return normalizeLocale(system);
|
|
279
|
+
return "zh-CN";
|
|
280
|
+
}
|
|
281
|
+
function loadCliPreprocessConfig(systemDir, locale) {
|
|
282
|
+
const preferences = new PreferencesManager(systemDir).load();
|
|
283
|
+
const config = preferences.aiPreprocessConfig;
|
|
284
|
+
return {
|
|
285
|
+
...config,
|
|
286
|
+
desensitizeRules: mergeRulesForLocale(
|
|
287
|
+
config.desensitizeRules,
|
|
288
|
+
locale,
|
|
289
|
+
config.desensitizeBuiltinRuleOverrides ?? {}
|
|
290
|
+
)
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/query/resolve.ts
|
|
295
|
+
function listSessionCandidates(source) {
|
|
296
|
+
const result = [];
|
|
297
|
+
for (const id of source.listSessionIds()) {
|
|
298
|
+
const db = source.open(id);
|
|
299
|
+
if (!db) continue;
|
|
300
|
+
const meta = getSessionMeta(db);
|
|
301
|
+
if (!meta) continue;
|
|
302
|
+
result.push({ id, name: meta.name, platform: meta.platform, db });
|
|
303
|
+
}
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
function resolveSession(source, ref) {
|
|
307
|
+
if (ref) {
|
|
308
|
+
const direct = source.open(ref);
|
|
309
|
+
if (direct && getSessionMeta(direct)) {
|
|
310
|
+
const meta = getSessionMeta(direct);
|
|
311
|
+
return { id: ref, name: meta.name, db: direct };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const sessions = listSessionCandidates(source);
|
|
315
|
+
if (!ref) {
|
|
316
|
+
if (sessions.length === 1) {
|
|
317
|
+
return { id: sessions[0].id, name: sessions[0].name, db: sessions[0].db };
|
|
318
|
+
}
|
|
319
|
+
if (sessions.length === 0) {
|
|
320
|
+
throw new QueryError({
|
|
321
|
+
code: "SESSION_NOT_FOUND",
|
|
322
|
+
message: "No chat sessions found",
|
|
323
|
+
hint: "Import a chat export first (chatlab import <file>)"
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
throw new QueryError({
|
|
327
|
+
code: "SESSION_AMBIGUOUS",
|
|
328
|
+
message: `${sessions.length} sessions available, --session is required`,
|
|
329
|
+
hint: "Retry with --session <id>",
|
|
330
|
+
candidates: sessions.map(({ id, name, platform }) => ({ id, name, platform }))
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const nameMatches = sessions.filter((s) => s.name === ref);
|
|
334
|
+
if (nameMatches.length === 1) {
|
|
335
|
+
return { id: nameMatches[0].id, name: nameMatches[0].name, db: nameMatches[0].db };
|
|
336
|
+
}
|
|
337
|
+
if (nameMatches.length > 1) {
|
|
338
|
+
throw new QueryError({
|
|
339
|
+
code: "SESSION_AMBIGUOUS",
|
|
340
|
+
message: `Session name '${ref}' matches ${nameMatches.length} sessions`,
|
|
341
|
+
hint: "Retry with --session <id>",
|
|
342
|
+
candidates: nameMatches.map(({ id, name, platform }) => ({ id, name, platform }))
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const partial = sessions.filter((s) => s.name.includes(ref));
|
|
346
|
+
if (partial.length > 0) {
|
|
347
|
+
throw new QueryError({
|
|
348
|
+
code: "SESSION_AMBIGUOUS",
|
|
349
|
+
message: `Session '${ref}' not found, ${partial.length} similar session(s) exist`,
|
|
350
|
+
hint: "Retry with --session <id>",
|
|
351
|
+
candidates: partial.map(({ id, name, platform }) => ({ id, name, platform }))
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
throw new QueryError({
|
|
355
|
+
code: "SESSION_NOT_FOUND",
|
|
356
|
+
message: `Session '${ref}' not found`,
|
|
357
|
+
hint: "Run `chatlab sessions list` to see available sessions"
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
function resolveMember(db, ref) {
|
|
361
|
+
if (ref === "me") {
|
|
362
|
+
const owner = resolveOwnerMember(db);
|
|
363
|
+
if (!owner) {
|
|
364
|
+
throw new QueryError({
|
|
365
|
+
code: "MEMBER_NOT_FOUND",
|
|
366
|
+
message: "Owner profile is not configured for this session",
|
|
367
|
+
hint: "Set the owner in the ChatLab app, or use an explicit --member <id>"
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return { id: owner.id, name: owner.name };
|
|
371
|
+
}
|
|
372
|
+
const members = getMembersWithAliases(db);
|
|
373
|
+
const displayName = (m) => m.groupNickname || m.accountName || m.platformId;
|
|
374
|
+
if (/^\d+$/.test(ref)) {
|
|
375
|
+
const byId = members.find((m) => m.id === Number(ref));
|
|
376
|
+
if (byId) return { id: byId.id, name: displayName(byId) };
|
|
377
|
+
throw new QueryError({
|
|
378
|
+
code: "MEMBER_NOT_FOUND",
|
|
379
|
+
message: `Member id ${ref} not found`,
|
|
380
|
+
hint: "Run `chatlab members list` to see available members"
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
const exact = members.filter(
|
|
384
|
+
(m) => m.groupNickname === ref || m.accountName === ref || m.aliases.includes(ref) || m.platformId === ref
|
|
385
|
+
);
|
|
386
|
+
if (exact.length === 1) {
|
|
387
|
+
return { id: exact[0].id, name: displayName(exact[0]) };
|
|
388
|
+
}
|
|
389
|
+
const pool = exact.length > 1 ? exact : members.filter((m) => displayName(m).includes(ref));
|
|
390
|
+
if (pool.length > 0) {
|
|
391
|
+
throw new QueryError({
|
|
392
|
+
code: "MEMBER_AMBIGUOUS",
|
|
393
|
+
message: `Member name '${ref}' matches ${pool.length} member(s)`,
|
|
394
|
+
hint: "Retry with --member <id>",
|
|
395
|
+
candidates: pool.slice(0, 10).map((m) => ({ id: m.id, name: displayName(m), messages: m.messageCount }))
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
throw new QueryError({
|
|
399
|
+
code: "MEMBER_NOT_FOUND",
|
|
400
|
+
message: `Member '${ref}' not found`,
|
|
401
|
+
hint: "Run `chatlab members list` to see available members"
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/query/context.ts
|
|
406
|
+
function createQueryContext(options) {
|
|
407
|
+
const { config, pathProvider, dbManager } = initRuntime();
|
|
408
|
+
const resolved = resolveSession(dbManager, options.session);
|
|
409
|
+
const locale = resolveCliLocale(config.locale.lang);
|
|
410
|
+
const preprocessConfig = loadCliPreprocessConfig(pathProvider.getSystemDir(), locale);
|
|
411
|
+
const ownerPlatformId = getSessionMeta(resolved.db)?.ownerId ?? void 0;
|
|
412
|
+
initNlpDir(path.join(pathProvider.getSystemDir(), "nlp"));
|
|
413
|
+
return {
|
|
414
|
+
db: resolved.db,
|
|
415
|
+
session: { id: resolved.id, name: resolved.name },
|
|
416
|
+
locale,
|
|
417
|
+
preprocessConfig,
|
|
418
|
+
ownerPlatformId,
|
|
419
|
+
allowRaw: config.cli.allow_raw,
|
|
420
|
+
allowSql: config.cli.allow_sql,
|
|
421
|
+
close: () => dbManager.closeAll()
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function assertRawAllowed(ctx, options) {
|
|
425
|
+
if (options.raw && !ctx.allowRaw) {
|
|
426
|
+
throw new QueryError({
|
|
427
|
+
code: "RAW_DISABLED",
|
|
428
|
+
message: "--raw output is disabled by default",
|
|
429
|
+
hint: "The user must opt in: set CHATLAB_CLI_ALLOW_RAW=1 or `chatlab config set cli.allow_raw true`"
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/query/parse.ts
|
|
435
|
+
import { createHash } from "crypto";
|
|
436
|
+
function toIsoWithOffset(date) {
|
|
437
|
+
const pad = (n, w = 2) => String(Math.abs(n)).padStart(w, "0");
|
|
438
|
+
const offsetMin = -date.getTimezoneOffset();
|
|
439
|
+
const sign = offsetMin >= 0 ? "+" : "-";
|
|
440
|
+
const offH = pad(Math.floor(Math.abs(offsetMin) / 60));
|
|
441
|
+
const offM = pad(Math.abs(offsetMin) % 60);
|
|
442
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}${sign}${offH}:${offM}`;
|
|
443
|
+
}
|
|
444
|
+
function epochToIso(ts) {
|
|
445
|
+
return toIsoWithOffset(new Date(ts * 1e3));
|
|
446
|
+
}
|
|
447
|
+
var DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
448
|
+
var DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?$/;
|
|
449
|
+
var ISO_DATE_PREFIX = /^(\d{4})-(\d{2})-(\d{2})T/;
|
|
450
|
+
var LAST_DURATION = /^(\d+)([hdw])$/;
|
|
451
|
+
function startOfDay(base, dayOffset = 0) {
|
|
452
|
+
return new Date(base.getFullYear(), base.getMonth(), base.getDate() + dayOffset);
|
|
453
|
+
}
|
|
454
|
+
function invalidTime(flag, value) {
|
|
455
|
+
return new QueryError({
|
|
456
|
+
code: "INVALID_ARGUMENT",
|
|
457
|
+
message: `Invalid ${flag} value: ${value}`,
|
|
458
|
+
hint: 'Use YYYY-MM-DD, "YYYY-MM-DD HH:mm", ISO 8601, or the keywords today/yesterday'
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function isSameLocalDate(date, y, m, d) {
|
|
462
|
+
return date.getFullYear() === y && date.getMonth() === m - 1 && date.getDate() === d;
|
|
463
|
+
}
|
|
464
|
+
function isSameLocalDateTime(date, y, m, d, h, min, s) {
|
|
465
|
+
return isSameLocalDate(date, y, m, d) && date.getHours() === h && date.getMinutes() === min && date.getSeconds() === s;
|
|
466
|
+
}
|
|
467
|
+
function parseTimeValue(value, boundary, flag, now) {
|
|
468
|
+
if (value === "today" || value === "yesterday") {
|
|
469
|
+
const dayOffset = value === "yesterday" ? -1 : 0;
|
|
470
|
+
if (boundary === "start") return startOfDay(now, dayOffset);
|
|
471
|
+
return new Date(startOfDay(now, dayOffset + 1).getTime() - 1e3);
|
|
472
|
+
}
|
|
473
|
+
const dateOnly = DATE_ONLY.exec(value);
|
|
474
|
+
if (dateOnly) {
|
|
475
|
+
const [, y, m, d] = dateOnly;
|
|
476
|
+
const year = Number(y);
|
|
477
|
+
const month = Number(m);
|
|
478
|
+
const day = Number(d);
|
|
479
|
+
const base = new Date(year, month - 1, day);
|
|
480
|
+
if (Number.isNaN(base.getTime()) || !isSameLocalDate(base, year, month, day)) throw invalidTime(flag, value);
|
|
481
|
+
if (boundary === "start") return base;
|
|
482
|
+
return new Date(new Date(year, month - 1, day + 1).getTime() - 1e3);
|
|
483
|
+
}
|
|
484
|
+
const dateTime = DATE_TIME.exec(value);
|
|
485
|
+
if (dateTime) {
|
|
486
|
+
const [, y, m, d, h, min, s] = dateTime;
|
|
487
|
+
const year = Number(y);
|
|
488
|
+
const month = Number(m);
|
|
489
|
+
const day = Number(d);
|
|
490
|
+
const hour = Number(h);
|
|
491
|
+
const minute = Number(min);
|
|
492
|
+
const second = Number(s ?? 0);
|
|
493
|
+
const parsed2 = new Date(year, month - 1, day, hour, minute, second);
|
|
494
|
+
if (Number.isNaN(parsed2.getTime()) || !isSameLocalDateTime(parsed2, year, month, day, hour, minute, second)) {
|
|
495
|
+
throw invalidTime(flag, value);
|
|
496
|
+
}
|
|
497
|
+
return parsed2;
|
|
498
|
+
}
|
|
499
|
+
const parsed = new Date(value);
|
|
500
|
+
if (!Number.isNaN(parsed.getTime()) && /\d{4}-\d{2}-\d{2}T/.test(value)) {
|
|
501
|
+
const isoDate = ISO_DATE_PREFIX.exec(value);
|
|
502
|
+
if (isoDate) {
|
|
503
|
+
const [, y, m, d] = isoDate;
|
|
504
|
+
const year = Number(y);
|
|
505
|
+
const month = Number(m);
|
|
506
|
+
const day = Number(d);
|
|
507
|
+
const localDate = new Date(year, month - 1, day);
|
|
508
|
+
if (!isSameLocalDate(localDate, year, month, day)) throw invalidTime(flag, value);
|
|
509
|
+
}
|
|
510
|
+
return parsed;
|
|
511
|
+
}
|
|
512
|
+
throw invalidTime(flag, value);
|
|
513
|
+
}
|
|
514
|
+
function parseTimeOptions(options, now = /* @__PURE__ */ new Date()) {
|
|
515
|
+
const { since, until, last } = options;
|
|
516
|
+
if (last !== void 0 && (since !== void 0 || until !== void 0)) {
|
|
517
|
+
throw new QueryError({
|
|
518
|
+
code: "INVALID_ARGUMENT",
|
|
519
|
+
message: "--last cannot be combined with --since/--until",
|
|
520
|
+
hint: "Use either --last <N>h|d|w or explicit --since/--until bounds"
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
if (last !== void 0) {
|
|
524
|
+
const match = LAST_DURATION.exec(last);
|
|
525
|
+
if (!match) {
|
|
526
|
+
throw new QueryError({
|
|
527
|
+
code: "INVALID_ARGUMENT",
|
|
528
|
+
message: `Invalid --last value: ${last}`,
|
|
529
|
+
hint: "Supported units: h (hours), d (days), w (weeks), e.g. --last 30d"
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
const n = Number(match[1]);
|
|
533
|
+
const unitSeconds = match[2] === "h" ? 3600 : match[2] === "d" ? 86400 : 604800;
|
|
534
|
+
const start = new Date(now.getTime() - n * unitSeconds * 1e3);
|
|
535
|
+
return {
|
|
536
|
+
startTs: Math.floor(start.getTime() / 1e3),
|
|
537
|
+
meta: { since: toIsoWithOffset(start), until: toIsoWithOffset(now) }
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
let startTs;
|
|
541
|
+
let endTs;
|
|
542
|
+
let sinceIso = null;
|
|
543
|
+
if (since !== void 0) {
|
|
544
|
+
const parsed = parseTimeValue(since, "start", "--since", now);
|
|
545
|
+
startTs = Math.floor(parsed.getTime() / 1e3);
|
|
546
|
+
sinceIso = toIsoWithOffset(parsed);
|
|
547
|
+
}
|
|
548
|
+
if (until !== void 0) {
|
|
549
|
+
const parsed = parseTimeValue(until, "end", "--until", now);
|
|
550
|
+
endTs = Math.floor(parsed.getTime() / 1e3);
|
|
551
|
+
}
|
|
552
|
+
if (startTs !== void 0 && endTs !== void 0 && startTs > endTs) {
|
|
553
|
+
throw new QueryError({
|
|
554
|
+
code: "INVALID_ARGUMENT",
|
|
555
|
+
message: "--since is after --until",
|
|
556
|
+
hint: "Check the time range boundaries"
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
startTs,
|
|
561
|
+
endTs,
|
|
562
|
+
meta: {
|
|
563
|
+
since: sinceIso,
|
|
564
|
+
until: endTs !== void 0 ? epochToIso(endTs) : toIsoWithOffset(now)
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function parseLimit(value, defaultValue, cap, flag, min = 0) {
|
|
569
|
+
if (value === void 0) return Math.min(defaultValue, cap);
|
|
570
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
571
|
+
if (!Number.isInteger(n) || n < min) {
|
|
572
|
+
throw new QueryError({
|
|
573
|
+
code: "INVALID_ARGUMENT",
|
|
574
|
+
message: `Invalid ${flag} value: ${value}`,
|
|
575
|
+
hint: min > 0 ? `${flag} must be an integer from ${min} to ${cap}` : `${flag} must be a non-negative integer (max ${cap})`
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
return Math.min(n, cap);
|
|
579
|
+
}
|
|
580
|
+
function queryFingerprint(conditions) {
|
|
581
|
+
const canonical = JSON.stringify(conditions, Object.keys(conditions).sort());
|
|
582
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 12);
|
|
583
|
+
}
|
|
584
|
+
function freezeTimeRangeForCursor(time) {
|
|
585
|
+
if (time.endTs !== void 0 || time.meta.until === null) return time;
|
|
586
|
+
const endMs = new Date(time.meta.until).getTime();
|
|
587
|
+
if (Number.isNaN(endMs)) return time;
|
|
588
|
+
return { ...time, endTs: Math.floor(endMs / 1e3) };
|
|
589
|
+
}
|
|
590
|
+
function resolveTimeOptionsForCursor(options, now = /* @__PURE__ */ new Date()) {
|
|
591
|
+
const snapshot = options.cursor ? getCursorSnapshot(options.cursor) : void 0;
|
|
592
|
+
return snapshot?.time ?? freezeTimeRangeForCursor(parseTimeOptions(options, now));
|
|
593
|
+
}
|
|
594
|
+
function encodeCursor(offset, fingerprint, snapshot) {
|
|
595
|
+
if (snapshot?.time) {
|
|
596
|
+
return Buffer.from(JSON.stringify({ v: 2, fp: fingerprint, offset, time: snapshot.time }), "utf8").toString(
|
|
597
|
+
"base64url"
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return Buffer.from(`${fingerprint}:${offset}`, "utf8").toString("base64url");
|
|
601
|
+
}
|
|
602
|
+
function asObject(value) {
|
|
603
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
604
|
+
}
|
|
605
|
+
function decodeCursorEnvelope(token) {
|
|
606
|
+
let decoded = "";
|
|
607
|
+
try {
|
|
608
|
+
decoded = Buffer.from(token, "base64url").toString("utf8");
|
|
609
|
+
} catch {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
let payload;
|
|
613
|
+
try {
|
|
614
|
+
payload = JSON.parse(decoded);
|
|
615
|
+
} catch {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
const object = asObject(payload);
|
|
619
|
+
if (!object || object.v !== 2 || typeof object.fp !== "string" || typeof object.offset !== "number" || !Number.isInteger(object.offset) || object.offset < 0) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
const snapshot = decodeCursorSnapshot(object);
|
|
623
|
+
return { fingerprint: object.fp, offset: object.offset, ...snapshot ? { snapshot } : {} };
|
|
624
|
+
}
|
|
625
|
+
function decodeCursorSnapshot(payload) {
|
|
626
|
+
const timeObject = asObject(payload.time);
|
|
627
|
+
if (!timeObject) return void 0;
|
|
628
|
+
const metaObject = asObject(timeObject.meta);
|
|
629
|
+
if (!metaObject) return void 0;
|
|
630
|
+
const startTs = timeObject.startTs;
|
|
631
|
+
const endTs = timeObject.endTs;
|
|
632
|
+
const since = metaObject.since;
|
|
633
|
+
const until = metaObject.until;
|
|
634
|
+
if (startTs !== void 0 && typeof startTs !== "number") return void 0;
|
|
635
|
+
if (endTs !== void 0 && typeof endTs !== "number") return void 0;
|
|
636
|
+
if (since !== null && typeof since !== "string") return void 0;
|
|
637
|
+
if (until !== null && typeof until !== "string") return void 0;
|
|
638
|
+
return {
|
|
639
|
+
time: {
|
|
640
|
+
...startTs !== void 0 ? { startTs } : {},
|
|
641
|
+
...endTs !== void 0 ? { endTs } : {},
|
|
642
|
+
meta: { since, until }
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function getCursorSnapshot(token) {
|
|
647
|
+
return decodeCursorEnvelope(token)?.snapshot;
|
|
648
|
+
}
|
|
649
|
+
function decodeCursor(token, fingerprint) {
|
|
650
|
+
const envelope = decodeCursorEnvelope(token);
|
|
651
|
+
if (envelope) {
|
|
652
|
+
if (envelope.fingerprint !== fingerprint) {
|
|
653
|
+
throw new QueryError({
|
|
654
|
+
code: "CURSOR_INVALID",
|
|
655
|
+
message: "Cursor does not match the current query conditions",
|
|
656
|
+
hint: "Cursors are only valid for the exact query that produced them; re-run the query without --cursor"
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
return envelope.offset;
|
|
660
|
+
}
|
|
661
|
+
let decoded = "";
|
|
662
|
+
try {
|
|
663
|
+
decoded = Buffer.from(token, "base64url").toString("utf8");
|
|
664
|
+
} catch {
|
|
665
|
+
}
|
|
666
|
+
const match = /^([0-9a-f]{12}):(\d+)$/.exec(decoded);
|
|
667
|
+
if (!match || match[1] !== fingerprint) {
|
|
668
|
+
throw new QueryError({
|
|
669
|
+
code: "CURSOR_INVALID",
|
|
670
|
+
message: "Cursor does not match the current query conditions",
|
|
671
|
+
hint: "Cursors are only valid for the exact query that produced them; re-run the query without --cursor"
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return Number(match[2]);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/query/commands-sessions-members.ts
|
|
678
|
+
function epochOrNull(ts) {
|
|
679
|
+
return ts == null ? null : epochToIso(ts);
|
|
680
|
+
}
|
|
681
|
+
function registerSessionCommands(program2) {
|
|
682
|
+
const sessionsCmd = program2.command("sessions").description("Chat sessions (list, show)").option("--format <format>", "Output format (table|json for legacy list; agent|json|text for subcommands)").action((options) => {
|
|
683
|
+
console.error("[Deprecated] `chatlab sessions` \u2014 use `chatlab sessions list`; alias kept for one major version");
|
|
684
|
+
legacySessionsList(options);
|
|
685
|
+
});
|
|
686
|
+
sessionsCmd.command("list").description("List imported chat sessions").option("--query <kw>", "Filter by session name substring").option("--platform <p>", "Filter by platform id").option("--format <format>", "Output format: agent|json|text").action(async (options) => {
|
|
687
|
+
await runQuery("sessions.list", options, async () => {
|
|
688
|
+
const { dbManager } = initRuntime();
|
|
689
|
+
try {
|
|
690
|
+
const items = dbManager.listSessionIds().map((id) => {
|
|
691
|
+
const db = dbManager.open(id);
|
|
692
|
+
if (!db) return null;
|
|
693
|
+
const meta = getSessionMeta(db);
|
|
694
|
+
if (!meta) return null;
|
|
695
|
+
const overview = getSessionOverview(db);
|
|
696
|
+
return {
|
|
697
|
+
id,
|
|
698
|
+
name: meta.name,
|
|
699
|
+
platform: meta.platform,
|
|
700
|
+
type: meta.type,
|
|
701
|
+
totalMessages: overview.totalMessages,
|
|
702
|
+
totalMembers: overview.totalMembers,
|
|
703
|
+
firstMessage: epochOrNull(overview.firstMessageTs),
|
|
704
|
+
lastMessage: epochOrNull(overview.lastMessageTs)
|
|
705
|
+
};
|
|
706
|
+
}).filter((s) => s !== null).filter((s) => options.query ? s.name.includes(options.query) : true).filter((s) => options.platform ? s.platform === options.platform : true);
|
|
707
|
+
return {
|
|
708
|
+
data: { items },
|
|
709
|
+
meta: { returned: items.length },
|
|
710
|
+
renderText: () => items.map(
|
|
711
|
+
(s) => `${s.id} ${s.name}
|
|
712
|
+
${s.platform} | ${s.type} | ${s.totalMembers} members | ${s.totalMessages} messages`
|
|
713
|
+
).join("\n") || "No chat sessions found."
|
|
714
|
+
};
|
|
715
|
+
} finally {
|
|
716
|
+
dbManager.closeAll();
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
sessionsCmd.command("show").description("Show one session in detail").option("--session <ref>", "Session id or unique name").option("--format <format>", "Output format: agent|json|text").action(async (options) => {
|
|
721
|
+
await runQuery("sessions.show", options, async () => {
|
|
722
|
+
const ctx = createQueryContext(options);
|
|
723
|
+
try {
|
|
724
|
+
const meta = getSessionMeta(ctx.db);
|
|
725
|
+
const overview = getSessionOverview(ctx.db);
|
|
726
|
+
const data = {
|
|
727
|
+
id: ctx.session.id,
|
|
728
|
+
name: meta.name,
|
|
729
|
+
platform: meta.platform,
|
|
730
|
+
type: meta.type,
|
|
731
|
+
totalMessages: overview.totalMessages,
|
|
732
|
+
totalMembers: overview.totalMembers,
|
|
733
|
+
firstMessage: epochOrNull(overview.firstMessageTs),
|
|
734
|
+
lastMessage: epochOrNull(overview.lastMessageTs)
|
|
735
|
+
};
|
|
736
|
+
return {
|
|
737
|
+
data,
|
|
738
|
+
meta: { session: ctx.session },
|
|
739
|
+
renderText: () => `${data.name} (${data.id})
|
|
740
|
+
Platform: ${data.platform} | Type: ${data.type}
|
|
741
|
+
Messages: ${data.totalMessages} | Members: ${data.totalMembers}
|
|
742
|
+
Range: ${data.firstMessage ?? "-"} ~ ${data.lastMessage ?? "-"}`
|
|
743
|
+
};
|
|
744
|
+
} finally {
|
|
745
|
+
ctx.close();
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
function registerMemberCommands(program2) {
|
|
751
|
+
const membersCmd = program2.command("members").description("Session members (list, history)").argument("[session-id]", "legacy: session id (deprecated alias)").option("--format <format>", "Output format").action((sessionId, options, command) => {
|
|
752
|
+
if (!sessionId) {
|
|
753
|
+
command.help({ error: false });
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
console.error(
|
|
757
|
+
"[Deprecated] `chatlab members <session-id>` \u2014 use `chatlab members list --session <id>`; alias kept for one major version"
|
|
758
|
+
);
|
|
759
|
+
legacyMembersList(sessionId, options);
|
|
760
|
+
});
|
|
761
|
+
membersCmd.command("list").description("List session members").option("--session <ref>", "Session id or unique name").option("--query <kw>", "Filter by member name substring").option("--sort <by>", "Sort by: messages|name", "messages").option("--limit <n>", "Max members to return (default 50)").option("--format <format>", "Output format: agent|json|text").action(async (options) => {
|
|
762
|
+
await runQuery("members.list", options, async () => {
|
|
763
|
+
const ctx = createQueryContext(options);
|
|
764
|
+
try {
|
|
765
|
+
const limit = parseLimit(options.limit, 50, 500, "--limit");
|
|
766
|
+
let members = getMembersWithAliases(ctx.db).map((m) => ({
|
|
767
|
+
id: m.id,
|
|
768
|
+
name: m.groupNickname || m.accountName || m.platformId,
|
|
769
|
+
...m.aliases.length > 0 ? { aliases: m.aliases } : {},
|
|
770
|
+
messageCount: m.messageCount
|
|
771
|
+
}));
|
|
772
|
+
if (options.query) {
|
|
773
|
+
const kw = options.query;
|
|
774
|
+
members = members.filter((m) => m.name.includes(kw) || m.aliases?.some((a) => a.includes(kw)));
|
|
775
|
+
}
|
|
776
|
+
if (options.sort === "name") {
|
|
777
|
+
members.sort((a, b) => a.name.localeCompare(b.name));
|
|
778
|
+
}
|
|
779
|
+
const total = members.length;
|
|
780
|
+
const items = members.slice(0, limit);
|
|
781
|
+
return {
|
|
782
|
+
data: { items },
|
|
783
|
+
meta: { session: ctx.session, total, returned: items.length },
|
|
784
|
+
renderText: () => items.map((m, i) => `${i + 1}. ${m.name} (#${m.id}) - ${m.messageCount} messages`).join("\n") || "No members."
|
|
785
|
+
};
|
|
786
|
+
} finally {
|
|
787
|
+
ctx.close();
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
membersCmd.command("history").description("Show name change history for a member").requiredOption("--member <ref>", "Member id, exact name, or `me`").option("--session <ref>", "Session id or unique name").option("--format <format>", "Output format: agent|json|text").action(async (options) => {
|
|
792
|
+
await runQuery("members.history", options, async () => {
|
|
793
|
+
const ctx = createQueryContext(options);
|
|
794
|
+
try {
|
|
795
|
+
const member = resolveMember(ctx.db, options.member);
|
|
796
|
+
const items = getMemberNameHistory(ctx.db, member.id).map((h) => ({
|
|
797
|
+
nameType: h.nameType,
|
|
798
|
+
name: h.name,
|
|
799
|
+
since: epochToIso(h.startTs),
|
|
800
|
+
until: h.endTs == null ? null : epochToIso(h.endTs)
|
|
801
|
+
}));
|
|
802
|
+
return {
|
|
803
|
+
data: { items },
|
|
804
|
+
meta: { session: ctx.session, member, returned: items.length },
|
|
805
|
+
renderText: () => items.map((h) => `${h.since} ~ ${h.until ?? "present"} [${h.nameType}] ${h.name}`).join("\n") || "No name changes recorded."
|
|
806
|
+
};
|
|
807
|
+
} finally {
|
|
808
|
+
ctx.close();
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
function legacySessionsList(options) {
|
|
814
|
+
const { dbManager } = initRuntime();
|
|
815
|
+
const sessionIds = dbManager.listSessionIds();
|
|
816
|
+
if (sessionIds.length === 0) {
|
|
817
|
+
console.log("No chat sessions found.");
|
|
818
|
+
console.log(`Data directory: ${dbManager["pathProvider"].getUserDataDir()}`);
|
|
819
|
+
dbManager.closeAll();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const sessions = sessionIds.map((id) => {
|
|
823
|
+
const db = dbManager.open(id);
|
|
824
|
+
if (!db) return null;
|
|
825
|
+
const meta = getSessionMeta(db);
|
|
826
|
+
if (!meta) return null;
|
|
827
|
+
const overview = getSessionOverview(db);
|
|
828
|
+
return { id, ...meta, ...overview };
|
|
829
|
+
}).filter(Boolean);
|
|
830
|
+
if (options.format === "json") {
|
|
831
|
+
console.log(JSON.stringify(sessions, null, 2));
|
|
832
|
+
} else {
|
|
833
|
+
console.log(`${sessions.length} session(s) found:
|
|
834
|
+
`);
|
|
835
|
+
for (const s of sessions) {
|
|
836
|
+
if (!s) continue;
|
|
837
|
+
const range = s.firstMessageTs && s.lastMessageTs ? `${new Date(s.firstMessageTs * 1e3).toLocaleDateString()} ~ ${new Date(s.lastMessageTs * 1e3).toLocaleDateString()}` : "";
|
|
838
|
+
console.log(` ${s.name}`);
|
|
839
|
+
console.log(` ID: ${s.id}`);
|
|
840
|
+
console.log(
|
|
841
|
+
` Platform: ${s.platform} | Type: ${s.type} | Members: ${s.totalMembers} | Messages: ${s.totalMessages}`
|
|
842
|
+
);
|
|
843
|
+
if (range) console.log(` Range: ${range}`);
|
|
844
|
+
console.log();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
dbManager.closeAll();
|
|
848
|
+
}
|
|
849
|
+
function legacyMembersList(sessionId, options) {
|
|
850
|
+
const { dbManager } = initRuntime();
|
|
851
|
+
const db = dbManager.open(sessionId);
|
|
852
|
+
if (!db) {
|
|
853
|
+
console.error(`Session ${sessionId} not found`);
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
const members = getMembers(db);
|
|
857
|
+
if (options.format === "json") {
|
|
858
|
+
console.log(JSON.stringify(members, null, 2));
|
|
859
|
+
} else {
|
|
860
|
+
console.log(`${members.length} member(s):
|
|
861
|
+
`);
|
|
862
|
+
for (const [i, m] of members.entries()) {
|
|
863
|
+
console.log(` ${i + 1}. ${m.name} (${m.platformId}) - ${m.messageCount} messages`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
dbManager.closeAll();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// src/query/messages-output.ts
|
|
870
|
+
var MAX_TOKENS_DEFAULT = 4e3;
|
|
871
|
+
var MAX_TOKENS_CAP = 32e3;
|
|
872
|
+
function privacyOnlyConfig(config) {
|
|
873
|
+
return { ...config, mergeConsecutive: false, denoise: false, anonymizeNames: false };
|
|
874
|
+
}
|
|
875
|
+
function assertRawFormatCompatible(format, options) {
|
|
876
|
+
if (options.raw && format === "agent") {
|
|
877
|
+
throw new QueryError({
|
|
878
|
+
code: "INVALID_ARGUMENT",
|
|
879
|
+
message: "--raw cannot be combined with --format agent",
|
|
880
|
+
hint: "Raw output is a debugging path; use --format json or --format text with --raw"
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function buildAgentText(messages, ctx, options) {
|
|
885
|
+
const maxTokens = parseLimit(options.maxTokens, MAX_TOKENS_DEFAULT, MAX_TOKENS_CAP, "--max-tokens");
|
|
886
|
+
const maxChars = options.full ? 0 : options.maxChars !== void 0 ? parseLimit(options.maxChars, 200, 1e4, "--max-chars") : options.defaultMaxChars;
|
|
887
|
+
const result = applyPreprocessingPipeline({
|
|
888
|
+
rawMessages: messages,
|
|
889
|
+
preprocessConfig: ctx.preprocessConfig,
|
|
890
|
+
locale: ctx.locale,
|
|
891
|
+
anonymizeNames: ctx.preprocessConfig.anonymizeNames ?? false,
|
|
892
|
+
ownerPlatformId: ctx.ownerPlatformId,
|
|
893
|
+
maxToolResultTokens: maxTokens,
|
|
894
|
+
truncationStrategy: options.strategy,
|
|
895
|
+
includeMessageIds: true,
|
|
896
|
+
hitIds: options.hitIds,
|
|
897
|
+
maxContentChars: maxChars
|
|
898
|
+
});
|
|
899
|
+
const config = ctx.preprocessConfig;
|
|
900
|
+
const preprocess = {
|
|
901
|
+
cleaned: config.dataCleaning !== false,
|
|
902
|
+
denoised: !!config.denoise,
|
|
903
|
+
merged: !!config.mergeConsecutive,
|
|
904
|
+
desensitized: !!config.desensitize && config.desensitizeRules.some((r) => r.enabled),
|
|
905
|
+
truncated: result.stats.truncated
|
|
906
|
+
};
|
|
907
|
+
const preprocessDetail = options.verbose ? {
|
|
908
|
+
inputMessages: result.stats.input,
|
|
909
|
+
renderedBlocks: result.stats.renderedBlocks,
|
|
910
|
+
cleaned: result.stats.cleaned,
|
|
911
|
+
blacklistRemoved: result.stats.blacklistRemoved,
|
|
912
|
+
denoiseRemoved: result.stats.denoiseRemoved,
|
|
913
|
+
mergeCombined: result.stats.mergeCombined,
|
|
914
|
+
desensitizeRulesApplied: result.stats.desensitizeRulesApplied
|
|
915
|
+
} : void 0;
|
|
916
|
+
return { text: result.text, stats: result.stats, preprocess, preprocessDetail };
|
|
917
|
+
}
|
|
918
|
+
var JSON_ITEM_FIELDS = /* @__PURE__ */ new Set(["id", "time", "senderId", "senderName", "type", "content", "hit"]);
|
|
919
|
+
function toJsonItems(messages, preprocessConfig, options) {
|
|
920
|
+
const visible = options.raw ? messages : preprocessMessages(messages, privacyOnlyConfig(preprocessConfig));
|
|
921
|
+
const noContent = options.content === false;
|
|
922
|
+
const maxChars = options.full ? 0 : options.maxChars !== void 0 ? parseLimit(options.maxChars, 300, 1e4, "--max-chars") : options.defaultMaxChars ?? 300;
|
|
923
|
+
let fields;
|
|
924
|
+
if (options.fields) {
|
|
925
|
+
fields = new Set(options.fields.split(",").map((f) => f.trim()));
|
|
926
|
+
for (const field of fields) {
|
|
927
|
+
if (!JSON_ITEM_FIELDS.has(field)) {
|
|
928
|
+
throw new QueryError({
|
|
929
|
+
code: "INVALID_ARGUMENT",
|
|
930
|
+
message: `Unknown field: ${field}`,
|
|
931
|
+
hint: `Supported fields: ${[...JSON_ITEM_FIELDS].join(", ")}`
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return visible.map((msg) => {
|
|
937
|
+
let content = msg.content;
|
|
938
|
+
if (noContent) {
|
|
939
|
+
content = void 0;
|
|
940
|
+
} else if (content && maxChars > 0 && content.length > maxChars) {
|
|
941
|
+
content = content.slice(0, maxChars) + "...";
|
|
942
|
+
}
|
|
943
|
+
const item = {
|
|
944
|
+
id: msg.id,
|
|
945
|
+
time: epochToIso(msg.timestamp),
|
|
946
|
+
...msg.senderId !== void 0 ? { senderId: msg.senderId } : {},
|
|
947
|
+
senderName: msg.senderName,
|
|
948
|
+
...msg.type !== void 0 ? { type: messageTypeToString(msg.type) } : {},
|
|
949
|
+
...content !== void 0 ? { content } : {},
|
|
950
|
+
...options.hitIds?.has(msg.id) ? { hit: true } : {}
|
|
951
|
+
};
|
|
952
|
+
if (fields) {
|
|
953
|
+
for (const key of Object.keys(item)) {
|
|
954
|
+
if (!fields.has(key)) delete item[key];
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return item;
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
function sortChronological(messages) {
|
|
961
|
+
return [...messages].sort((a, b) => a.timestamp - b.timestamp || a.id - b.id);
|
|
962
|
+
}
|
|
963
|
+
function buildMessagesResult(format, ctx, options, messages, meta, shape) {
|
|
964
|
+
const chronological = sortChronological(messages);
|
|
965
|
+
if (format === "json") {
|
|
966
|
+
const items = toJsonItems(chronological, ctx.preprocessConfig, {
|
|
967
|
+
maxChars: options.maxChars,
|
|
968
|
+
full: options.full,
|
|
969
|
+
fields: options.fields,
|
|
970
|
+
content: options.content,
|
|
971
|
+
raw: options.raw,
|
|
972
|
+
hitIds: shape.hitIds,
|
|
973
|
+
defaultMaxChars: shape.defaultMaxChars
|
|
974
|
+
});
|
|
975
|
+
return {
|
|
976
|
+
data: { items },
|
|
977
|
+
meta: {
|
|
978
|
+
...meta,
|
|
979
|
+
returned: items.length,
|
|
980
|
+
preprocess: options.raw ? { raw: true } : {
|
|
981
|
+
cleaned: ctx.preprocessConfig.dataCleaning !== false,
|
|
982
|
+
desensitized: !!ctx.preprocessConfig.desensitize && ctx.preprocessConfig.desensitizeRules.some((r) => r.enabled)
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
if (options.raw) {
|
|
988
|
+
return {
|
|
989
|
+
data: { items: chronological },
|
|
990
|
+
renderText: () => chronological.map((m) => `[#${m.id}] ${new Date(m.timestamp * 1e3).toLocaleString()} ${m.senderName}: ${m.content ?? ""}`).join("\n") || "No messages."
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
const agent = buildAgentText(chronological, ctx, {
|
|
994
|
+
hitIds: shape.hitIds,
|
|
995
|
+
strategy: shape.strategy,
|
|
996
|
+
maxTokens: options.maxTokens,
|
|
997
|
+
maxChars: options.maxChars,
|
|
998
|
+
full: options.full,
|
|
999
|
+
verbose: options.verbose,
|
|
1000
|
+
defaultMaxChars: shape.defaultMaxChars
|
|
1001
|
+
});
|
|
1002
|
+
return {
|
|
1003
|
+
data: { text: agent.text },
|
|
1004
|
+
meta: {
|
|
1005
|
+
...meta,
|
|
1006
|
+
preprocess: agent.preprocess,
|
|
1007
|
+
...agent.preprocessDetail ? { preprocessDetail: agent.preprocessDetail } : {}
|
|
1008
|
+
},
|
|
1009
|
+
renderText: () => agent.text
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/query/commands-messages.ts
|
|
1014
|
+
function capExpandedSearchMessages(messages, hitIds, maxMessages) {
|
|
1015
|
+
if (maxMessages <= 0) return [];
|
|
1016
|
+
if (messages.length <= maxMessages) return messages;
|
|
1017
|
+
const capped = messages.slice(0, maxMessages);
|
|
1018
|
+
const includedIds = new Set(capped.map((message) => message.id));
|
|
1019
|
+
const missingHits = messages.filter((message) => hitIds.has(message.id) && !includedIds.has(message.id));
|
|
1020
|
+
for (const hit of missingHits) {
|
|
1021
|
+
let replaceIndex = -1;
|
|
1022
|
+
for (let i = capped.length - 1; i >= 0; i--) {
|
|
1023
|
+
if (!hitIds.has(capped[i].id)) {
|
|
1024
|
+
replaceIndex = i;
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (replaceIndex === -1) break;
|
|
1029
|
+
capped[replaceIndex] = hit;
|
|
1030
|
+
}
|
|
1031
|
+
return capped.sort((a, b) => messages.indexOf(a) - messages.indexOf(b));
|
|
1032
|
+
}
|
|
1033
|
+
function appendUniqueTailMessages(expanded, tail) {
|
|
1034
|
+
const expandedIds = new Set(expanded.map((message) => message.id));
|
|
1035
|
+
return [...expanded, ...tail.filter((message) => !expandedIds.has(message.id))];
|
|
1036
|
+
}
|
|
1037
|
+
function parseContextIds(value) {
|
|
1038
|
+
const tokens = value.split(",").map((token) => token.trim());
|
|
1039
|
+
const ids = tokens.map((token) => token.length > 0 ? Number(token) : Number.NaN);
|
|
1040
|
+
if (ids.length === 0 || ids.some((id) => !Number.isInteger(id) || id <= 0)) {
|
|
1041
|
+
throw new QueryError({
|
|
1042
|
+
code: "INVALID_ARGUMENT",
|
|
1043
|
+
message: `Invalid --id value: ${value}`,
|
|
1044
|
+
hint: "Pass positive numeric message ids, e.g. --id 1021 or --id 1021,1058"
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
return ids;
|
|
1048
|
+
}
|
|
1049
|
+
function parseSearchKeywords(values) {
|
|
1050
|
+
const keywords = values.map((value) => value.trim()).filter(Boolean);
|
|
1051
|
+
if (keywords.length === 0) {
|
|
1052
|
+
throw new QueryError({
|
|
1053
|
+
code: "INVALID_ARGUMENT",
|
|
1054
|
+
message: "Invalid search keywords",
|
|
1055
|
+
hint: "Pass at least one non-empty search keyword"
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
return keywords;
|
|
1059
|
+
}
|
|
1060
|
+
function searchTruncationStrategy(sort) {
|
|
1061
|
+
return sort === "desc" ? "keep_last" : "keep_first";
|
|
1062
|
+
}
|
|
1063
|
+
function assertContextAnchorsPresent(ids, messages, rawValue) {
|
|
1064
|
+
const returnedIds = new Set(messages.map((message) => message.id));
|
|
1065
|
+
if (ids.some((id) => !returnedIds.has(id))) {
|
|
1066
|
+
throw new QueryError({
|
|
1067
|
+
code: "MESSAGE_NOT_FOUND",
|
|
1068
|
+
message: `No messages found for id(s) ${rawValue}`,
|
|
1069
|
+
hint: "Use a single numeric id from [#id]/[#id*] markers; merged [#a-b] ranges are display-only"
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function addSharedOptions(cmd, options = {}) {
|
|
1074
|
+
const shared = cmd.option("--session <ref>", "Session id or unique name (auto-selected when only one exists)");
|
|
1075
|
+
if (options.includeTime !== false) {
|
|
1076
|
+
shared.option("--since <t>", 'Start time: YYYY-MM-DD, "YYYY-MM-DD HH:mm", ISO 8601, today, yesterday').option("--until <t>", "End time (date-only values include the whole day)").option("--last <dur>", "Relative window: <N>h|d|w (mutually exclusive with --since/--until)");
|
|
1077
|
+
}
|
|
1078
|
+
return shared.option("--format <format>", "Output format: agent|json|text (agents should pass agent explicitly)").option("--max-tokens <n>", "Token budget for agent text (default 4000)").option("--max-chars <n>", "Per-message content char limit").option("--full", "Disable per-message content truncation").option("--fields <a,b>", "json format: only include these item fields").option("--no-content", "json format: omit message content (distribution scouting)").option("--raw", "Bypass privacy preprocessing (debugging; requires user opt-in, json/text only)").option("--verbose", "Include preprocessing diagnostics in meta");
|
|
1079
|
+
}
|
|
1080
|
+
function registerMessageCommands(program2) {
|
|
1081
|
+
const messagesCmd = program2.command("messages").description("Query messages (list, search, context, between)");
|
|
1082
|
+
const listCmd = messagesCmd.command("list").description("List messages in a time window (most recent page first, rendered chronologically)").option("--member <ref>", "Filter by sender (member id, exact name, or `me`)").option("--cursor <token>", "Opaque pagination cursor from meta.nextCursor").option("--limit <n>", "Messages per page (default 50, max 500)");
|
|
1083
|
+
addSharedOptions(listCmd);
|
|
1084
|
+
listCmd.action(async (options) => {
|
|
1085
|
+
await runQuery("messages.list", options, async (format) => {
|
|
1086
|
+
assertRawFormatCompatible(format, options);
|
|
1087
|
+
const ctx = createQueryContext(options);
|
|
1088
|
+
try {
|
|
1089
|
+
assertRawAllowed(ctx, options);
|
|
1090
|
+
const time = resolveTimeOptionsForCursor(options);
|
|
1091
|
+
const member = options.member ? resolveMember(ctx.db, options.member) : void 0;
|
|
1092
|
+
const limit = parseLimit(options.limit, 50, 500, "--limit", 1);
|
|
1093
|
+
const excludeKeywords = options.raw ? void 0 : ctx.preprocessConfig.blacklistKeywords;
|
|
1094
|
+
const fingerprint = queryFingerprint({
|
|
1095
|
+
command: "messages.list",
|
|
1096
|
+
session: ctx.session.id,
|
|
1097
|
+
startTs: time.startTs ?? null,
|
|
1098
|
+
endTs: time.endTs ?? null,
|
|
1099
|
+
member: member?.id ?? null,
|
|
1100
|
+
exclude: excludeKeywords ?? []
|
|
1101
|
+
});
|
|
1102
|
+
const offset = options.cursor ? decodeCursor(options.cursor, fingerprint) : 0;
|
|
1103
|
+
const result = searchMessagesByKeywords(ctx.db, [], {
|
|
1104
|
+
startTs: time.startTs,
|
|
1105
|
+
endTs: time.endTs,
|
|
1106
|
+
senderId: member?.id,
|
|
1107
|
+
limit,
|
|
1108
|
+
offset,
|
|
1109
|
+
excludeKeywords,
|
|
1110
|
+
sort: "desc"
|
|
1111
|
+
});
|
|
1112
|
+
const total = result.total ?? result.messages.length;
|
|
1113
|
+
const hasMore = offset + result.messages.length < total;
|
|
1114
|
+
const meta = {
|
|
1115
|
+
session: ctx.session,
|
|
1116
|
+
timeRange: time.meta,
|
|
1117
|
+
...member ? { member } : {},
|
|
1118
|
+
totalHits: total,
|
|
1119
|
+
returnedHits: result.messages.length,
|
|
1120
|
+
hasMore,
|
|
1121
|
+
...hasMore ? { nextCursor: encodeCursor(offset + result.messages.length, fingerprint, { time }) } : {}
|
|
1122
|
+
};
|
|
1123
|
+
return buildMessagesResult(format, ctx, options, result.messages, meta, {
|
|
1124
|
+
strategy: "keep_last"
|
|
1125
|
+
});
|
|
1126
|
+
} finally {
|
|
1127
|
+
ctx.close();
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
const searchCmd = messagesCmd.command("search").description("Search messages by keywords (LIKE match; multiple keywords default to OR)").argument("<keywords...>", "Search keywords").option("--match <mode>", "Keyword join mode: any (OR) | all (AND)", "any").option("--member <ref>", "Filter by sender (member id, exact name, or `me`)").option("--context <n>", "Context messages before/after each hit (default 0)").option("--sort <dir>", 'Hit order by time: asc|desc (asc for "who said it first")', "desc").option("--cursor <token>", "Opaque pagination cursor from meta.nextCursor").option("--limit <n>", "Hits per page (default 20, max 500)").option("--max-messages <n>", "Cap on total messages after context expansion (default 200)");
|
|
1132
|
+
addSharedOptions(searchCmd);
|
|
1133
|
+
searchCmd.action(
|
|
1134
|
+
async (keywords, options) => {
|
|
1135
|
+
await runQuery("messages.search", options, async (format) => {
|
|
1136
|
+
assertRawFormatCompatible(format, options);
|
|
1137
|
+
const searchKeywords = parseSearchKeywords(keywords);
|
|
1138
|
+
if (options.match !== "any" && options.match !== "all") {
|
|
1139
|
+
throw new QueryError({
|
|
1140
|
+
code: "INVALID_ARGUMENT",
|
|
1141
|
+
message: `Invalid --match value: ${options.match}`,
|
|
1142
|
+
hint: "Supported: any (OR), all (AND)"
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
if (options.sort !== "asc" && options.sort !== "desc") {
|
|
1146
|
+
throw new QueryError({
|
|
1147
|
+
code: "INVALID_ARGUMENT",
|
|
1148
|
+
message: `Invalid --sort value: ${options.sort}`,
|
|
1149
|
+
hint: "Supported: asc, desc"
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
const ctx = createQueryContext(options);
|
|
1153
|
+
try {
|
|
1154
|
+
assertRawAllowed(ctx, options);
|
|
1155
|
+
const time = resolveTimeOptionsForCursor(options);
|
|
1156
|
+
const member = options.member ? resolveMember(ctx.db, options.member) : void 0;
|
|
1157
|
+
const limit = parseLimit(options.limit, 20, 500, "--limit", 1);
|
|
1158
|
+
const context = parseLimit(options.context, 0, 50, "--context");
|
|
1159
|
+
const maxMessages = parseLimit(options.maxMessages, 200, 2e3, "--max-messages");
|
|
1160
|
+
const excludeKeywords = options.raw ? void 0 : ctx.preprocessConfig.blacklistKeywords;
|
|
1161
|
+
const fingerprint = queryFingerprint({
|
|
1162
|
+
command: "messages.search",
|
|
1163
|
+
session: ctx.session.id,
|
|
1164
|
+
keywords: searchKeywords,
|
|
1165
|
+
match: options.match,
|
|
1166
|
+
sort: options.sort,
|
|
1167
|
+
startTs: time.startTs ?? null,
|
|
1168
|
+
endTs: time.endTs ?? null,
|
|
1169
|
+
member: member?.id ?? null,
|
|
1170
|
+
exclude: excludeKeywords ?? []
|
|
1171
|
+
});
|
|
1172
|
+
const offset = options.cursor ? decodeCursor(options.cursor, fingerprint) : 0;
|
|
1173
|
+
const result = searchMessagesByKeywords(ctx.db, searchKeywords, {
|
|
1174
|
+
startTs: time.startTs,
|
|
1175
|
+
endTs: time.endTs,
|
|
1176
|
+
senderId: member?.id,
|
|
1177
|
+
limit,
|
|
1178
|
+
offset,
|
|
1179
|
+
matchMode: options.match,
|
|
1180
|
+
excludeKeywords,
|
|
1181
|
+
sort: options.sort
|
|
1182
|
+
});
|
|
1183
|
+
const hits = result.messages;
|
|
1184
|
+
const hitIds = new Set(hits.map((m) => m.id));
|
|
1185
|
+
const warnings = [];
|
|
1186
|
+
let pipelineMessages = hits;
|
|
1187
|
+
if (context > 0 && hits.length > 0) {
|
|
1188
|
+
const perHitCost = context * 2 + 1;
|
|
1189
|
+
let expandableHits = hits;
|
|
1190
|
+
if (hits.length * perHitCost > maxMessages) {
|
|
1191
|
+
const allowed = Math.max(1, Math.floor(maxMessages / perHitCost));
|
|
1192
|
+
expandableHits = hits.slice(0, allowed);
|
|
1193
|
+
warnings.push(
|
|
1194
|
+
`--max-messages ${maxMessages}: context expanded for the first ${allowed} of ${hits.length} hits`
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
const expanded = getSearchMessageContext(
|
|
1198
|
+
ctx.db,
|
|
1199
|
+
expandableHits.map((m) => m.id),
|
|
1200
|
+
context,
|
|
1201
|
+
context
|
|
1202
|
+
);
|
|
1203
|
+
const keptHits = new Set(expandableHits.map((m) => m.id));
|
|
1204
|
+
const tail = hits.filter((m) => !keptHits.has(m.id));
|
|
1205
|
+
pipelineMessages = appendUniqueTailMessages(expanded, tail);
|
|
1206
|
+
if (pipelineMessages.length > maxMessages) {
|
|
1207
|
+
pipelineMessages = capExpandedSearchMessages(pipelineMessages, hitIds, maxMessages);
|
|
1208
|
+
warnings.push(`response capped at ${maxMessages} messages`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
const total = result.total ?? hits.length;
|
|
1212
|
+
const hasMore = offset + hits.length < total;
|
|
1213
|
+
const meta = {
|
|
1214
|
+
session: ctx.session,
|
|
1215
|
+
timeRange: time.meta,
|
|
1216
|
+
query: { keywords: searchKeywords, match: options.match, engine: "like", sort: options.sort, context },
|
|
1217
|
+
...member ? { member } : {},
|
|
1218
|
+
totalHits: total,
|
|
1219
|
+
returnedHits: hits.length,
|
|
1220
|
+
hasMore,
|
|
1221
|
+
...hasMore ? { nextCursor: encodeCursor(offset + hits.length, fingerprint, { time }) } : {},
|
|
1222
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
1223
|
+
};
|
|
1224
|
+
return buildMessagesResult(format, ctx, options, pipelineMessages, meta, {
|
|
1225
|
+
hitIds,
|
|
1226
|
+
strategy: searchTruncationStrategy(options.sort),
|
|
1227
|
+
defaultMaxChars: 120
|
|
1228
|
+
});
|
|
1229
|
+
} finally {
|
|
1230
|
+
ctx.close();
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
);
|
|
1235
|
+
const contextCmd = messagesCmd.command("context").description("Show messages around specific numeric message ids").requiredOption("--id <ids>", "Message id(s), comma-separated").option("--window <n>", "Messages before/after each id (default 10, max 100)");
|
|
1236
|
+
addSharedOptions(contextCmd, { includeTime: false });
|
|
1237
|
+
contextCmd.action(async (options) => {
|
|
1238
|
+
await runQuery("messages.context", options, async (format) => {
|
|
1239
|
+
assertRawFormatCompatible(format, options);
|
|
1240
|
+
const ids = parseContextIds(options.id);
|
|
1241
|
+
const ctx = createQueryContext(options);
|
|
1242
|
+
try {
|
|
1243
|
+
assertRawAllowed(ctx, options);
|
|
1244
|
+
const window = parseLimit(options.window, 10, 100, "--window");
|
|
1245
|
+
const messages = getMessageContext(ctx.db, ids, window);
|
|
1246
|
+
assertContextAnchorsPresent(ids, messages, options.id);
|
|
1247
|
+
const meta = {
|
|
1248
|
+
session: ctx.session,
|
|
1249
|
+
anchorIds: ids,
|
|
1250
|
+
window
|
|
1251
|
+
};
|
|
1252
|
+
return buildMessagesResult(format, ctx, options, messages, meta, {
|
|
1253
|
+
hitIds: new Set(ids),
|
|
1254
|
+
strategy: "keep_last"
|
|
1255
|
+
});
|
|
1256
|
+
} finally {
|
|
1257
|
+
ctx.close();
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
});
|
|
1261
|
+
const betweenCmd = messagesCmd.command("between").description("Conversation between two members (repeat --member exactly twice)").option("--member <ref>", "Member id, exact name, or `me` (repeat twice)", collectMembers, []).option("--cursor <token>", "Opaque pagination cursor from meta.nextCursor").option("--limit <n>", "Messages per page (default 50, max 500)");
|
|
1262
|
+
addSharedOptions(betweenCmd);
|
|
1263
|
+
betweenCmd.action(async (options) => {
|
|
1264
|
+
await runQuery("messages.between", options, async (format) => {
|
|
1265
|
+
assertRawFormatCompatible(format, options);
|
|
1266
|
+
if (options.member.length !== 2) {
|
|
1267
|
+
throw new QueryError({
|
|
1268
|
+
code: "INVALID_ARGUMENT",
|
|
1269
|
+
message: `--member must be given exactly twice (got ${options.member.length})`,
|
|
1270
|
+
hint: "Example: messages between --member me --member \u5C0F\u7EA2"
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
const ctx = createQueryContext(options);
|
|
1274
|
+
try {
|
|
1275
|
+
assertRawAllowed(ctx, options);
|
|
1276
|
+
const time = resolveTimeOptionsForCursor(options);
|
|
1277
|
+
const memberA = resolveMember(ctx.db, options.member[0]);
|
|
1278
|
+
const memberB = resolveMember(ctx.db, options.member[1]);
|
|
1279
|
+
const limit = parseLimit(options.limit, 50, 500, "--limit", 1);
|
|
1280
|
+
const excludeKeywords = options.raw ? void 0 : ctx.preprocessConfig.blacklistKeywords;
|
|
1281
|
+
const fingerprint = queryFingerprint({
|
|
1282
|
+
command: "messages.between",
|
|
1283
|
+
session: ctx.session.id,
|
|
1284
|
+
members: [memberA.id, memberB.id],
|
|
1285
|
+
startTs: time.startTs ?? null,
|
|
1286
|
+
endTs: time.endTs ?? null,
|
|
1287
|
+
exclude: excludeKeywords ?? []
|
|
1288
|
+
});
|
|
1289
|
+
const offset = options.cursor ? decodeCursor(options.cursor, fingerprint) : 0;
|
|
1290
|
+
const timeFilter = time.startTs !== void 0 || time.endTs !== void 0 ? { startTs: time.startTs ?? 0, endTs: time.endTs ?? Math.floor(Date.now() / 1e3) } : void 0;
|
|
1291
|
+
const result = getConversationBetween(ctx.db, memberA.id, memberB.id, timeFilter, limit, {
|
|
1292
|
+
offset,
|
|
1293
|
+
excludeKeywords
|
|
1294
|
+
});
|
|
1295
|
+
const returned = result.messages.length;
|
|
1296
|
+
const hasMore = offset + returned < result.total;
|
|
1297
|
+
const meta = {
|
|
1298
|
+
session: ctx.session,
|
|
1299
|
+
timeRange: time.meta,
|
|
1300
|
+
members: [memberA, memberB],
|
|
1301
|
+
totalHits: result.total,
|
|
1302
|
+
returnedHits: returned,
|
|
1303
|
+
hasMore,
|
|
1304
|
+
...hasMore ? { nextCursor: encodeCursor(offset + returned, fingerprint, { time }) } : {}
|
|
1305
|
+
};
|
|
1306
|
+
return buildMessagesResult(format, ctx, options, result.messages, meta, {
|
|
1307
|
+
strategy: "keep_last"
|
|
1308
|
+
});
|
|
1309
|
+
} finally {
|
|
1310
|
+
ctx.close();
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
function collectMembers(value, previous) {
|
|
1316
|
+
return [...previous, value];
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/query/commands-stats.ts
|
|
1320
|
+
function addStatsOptions(cmd) {
|
|
1321
|
+
return cmd.option("--session <ref>", "Session id or unique name (auto-selected when only one exists)").option("--since <t>", "Start time: YYYY-MM-DD, ISO 8601, today, yesterday").option("--until <t>", "End time (date-only values include the whole day)").option("--last <dur>", "Relative window: <N>h|d|w").option("--format <format>", "Output format: agent|json|text");
|
|
1322
|
+
}
|
|
1323
|
+
function toTimeFilter(time) {
|
|
1324
|
+
if (time.startTs === void 0 && time.endTs === void 0) return void 0;
|
|
1325
|
+
return { startTs: time.startTs ?? 0, endTs: time.endTs ?? Math.floor(Date.now() / 1e3) };
|
|
1326
|
+
}
|
|
1327
|
+
function responseStatsMessagesSql() {
|
|
1328
|
+
return `SELECT msg.id, msg.sender_id, COALESCE(m.group_nickname, m.account_name) AS name, msg.ts
|
|
1329
|
+
FROM message msg
|
|
1330
|
+
JOIN member m ON msg.sender_id = m.id
|
|
1331
|
+
WHERE msg.type = 0 AND msg.ts >= @startTs
|
|
1332
|
+
ORDER BY msg.ts ASC, msg.id ASC`;
|
|
1333
|
+
}
|
|
1334
|
+
function registerStatsCommands(program2) {
|
|
1335
|
+
const statsCmd = program2.command("stats").description("Session statistics (overview, activity, time, keywords, response)").argument("[session-id]", "legacy: session id (deprecated alias)").option("--format <format>", "Output format").option("--top <n>", "legacy: top N members", "10").action((sessionId, options, command) => {
|
|
1336
|
+
if (!sessionId) {
|
|
1337
|
+
command.help({ error: false });
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
console.error(
|
|
1341
|
+
"[Deprecated] `chatlab stats <session-id>` \u2014 use `chatlab stats overview --session <id>`; alias kept for one major version"
|
|
1342
|
+
);
|
|
1343
|
+
legacyStats(sessionId, options);
|
|
1344
|
+
});
|
|
1345
|
+
statsCmd.command("overview").description("Session overview: totals, time range, top members").option("--session <ref>", "Session id or unique name").option("--format <format>", "Output format: agent|json|text").action(async (options) => {
|
|
1346
|
+
await runQuery("stats.overview", options, async () => {
|
|
1347
|
+
const ctx = createQueryContext(options);
|
|
1348
|
+
try {
|
|
1349
|
+
const overview = getChatOverview(ctx.db);
|
|
1350
|
+
if (!overview) {
|
|
1351
|
+
throw new QueryError({ code: "SESSION_NOT_FOUND", message: "Session metadata missing" });
|
|
1352
|
+
}
|
|
1353
|
+
const data = {
|
|
1354
|
+
name: overview.name,
|
|
1355
|
+
platform: overview.platform,
|
|
1356
|
+
type: overview.type,
|
|
1357
|
+
totalMessages: overview.totalMessages,
|
|
1358
|
+
totalMembers: overview.totalMembers,
|
|
1359
|
+
firstMessage: overview.firstMessageTs == null ? null : epochToIso(overview.firstMessageTs),
|
|
1360
|
+
lastMessage: overview.lastMessageTs == null ? null : epochToIso(overview.lastMessageTs),
|
|
1361
|
+
topMembers: overview.topMembers,
|
|
1362
|
+
summaryCount: overview.summaryCount
|
|
1363
|
+
};
|
|
1364
|
+
return {
|
|
1365
|
+
data,
|
|
1366
|
+
meta: { session: ctx.session },
|
|
1367
|
+
renderText: () => `${data.name}
|
|
1368
|
+
Messages: ${data.totalMessages} | Members: ${data.totalMembers}
|
|
1369
|
+
Range: ${data.firstMessage ?? "-"} ~ ${data.lastMessage ?? "-"}
|
|
1370
|
+
` + data.topMembers.map((m, i) => ` ${i + 1}. ${m.name} - ${m.count}`).join("\n")
|
|
1371
|
+
};
|
|
1372
|
+
} finally {
|
|
1373
|
+
ctx.close();
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
});
|
|
1377
|
+
const activityCmd = statsCmd.command("activity").description("Member activity ranking");
|
|
1378
|
+
addStatsOptions(activityCmd);
|
|
1379
|
+
activityCmd.option("--top <n>", "Top N members (default 10, max 100)");
|
|
1380
|
+
activityCmd.action(async (options) => {
|
|
1381
|
+
await runQuery("stats.activity", options, async () => {
|
|
1382
|
+
const ctx = createQueryContext(options);
|
|
1383
|
+
try {
|
|
1384
|
+
const time = parseTimeOptions(options);
|
|
1385
|
+
const top = parseLimit(options.top, 10, 100, "--top");
|
|
1386
|
+
const items = getMemberActivity(ctx.db, toTimeFilter(time)).slice(0, top).map((m) => ({ id: m.memberId, name: m.name, messageCount: m.messageCount, percentage: m.percentage }));
|
|
1387
|
+
return {
|
|
1388
|
+
data: { items },
|
|
1389
|
+
meta: { session: ctx.session, timeRange: time.meta, returned: items.length },
|
|
1390
|
+
renderText: () => items.map((m, i) => `${i + 1}. ${m.name} - ${m.messageCount} messages (${m.percentage}%)`).join("\n") || "No activity."
|
|
1391
|
+
};
|
|
1392
|
+
} finally {
|
|
1393
|
+
ctx.close();
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
});
|
|
1397
|
+
const timeCmd = statsCmd.command("time").description("Message distribution over time");
|
|
1398
|
+
addStatsOptions(timeCmd);
|
|
1399
|
+
timeCmd.requiredOption("--by <unit>", "Bucket unit: hour|weekday|day|month");
|
|
1400
|
+
timeCmd.action(async (options) => {
|
|
1401
|
+
await runQuery("stats.time", options, async () => {
|
|
1402
|
+
const ctx = createQueryContext(options);
|
|
1403
|
+
try {
|
|
1404
|
+
const time = parseTimeOptions(options);
|
|
1405
|
+
const filter = toTimeFilter(time);
|
|
1406
|
+
let items;
|
|
1407
|
+
switch (options.by) {
|
|
1408
|
+
case "hour":
|
|
1409
|
+
items = getHourlyActivity(ctx.db, filter);
|
|
1410
|
+
break;
|
|
1411
|
+
case "weekday":
|
|
1412
|
+
items = getWeekdayActivity(ctx.db, filter);
|
|
1413
|
+
break;
|
|
1414
|
+
case "day":
|
|
1415
|
+
items = getDailyActivity(ctx.db, filter);
|
|
1416
|
+
break;
|
|
1417
|
+
case "month":
|
|
1418
|
+
items = getMonthlyActivity(ctx.db, filter);
|
|
1419
|
+
break;
|
|
1420
|
+
default:
|
|
1421
|
+
throw new QueryError({
|
|
1422
|
+
code: "INVALID_ARGUMENT",
|
|
1423
|
+
message: `Invalid --by value: ${options.by}`,
|
|
1424
|
+
hint: "Supported: hour, weekday, day, month"
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
return {
|
|
1428
|
+
data: { items },
|
|
1429
|
+
meta: { session: ctx.session, timeRange: time.meta, by: options.by, returned: items.length }
|
|
1430
|
+
};
|
|
1431
|
+
} finally {
|
|
1432
|
+
ctx.close();
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
const keywordsCmd = statsCmd.command("keywords").description("High-frequency words (privacy-filtered)");
|
|
1437
|
+
addStatsOptions(keywordsCmd);
|
|
1438
|
+
keywordsCmd.option("--top <n>", "Top N words (default 20, max 100)");
|
|
1439
|
+
keywordsCmd.option("--member <ref>", "Filter by sender (member id, exact name, or `me`)");
|
|
1440
|
+
keywordsCmd.action(async (options) => {
|
|
1441
|
+
await runQuery("stats.keywords", options, async () => {
|
|
1442
|
+
const ctx = createQueryContext(options);
|
|
1443
|
+
try {
|
|
1444
|
+
const time = parseTimeOptions(options);
|
|
1445
|
+
const top = parseLimit(options.top, 20, 100, "--top");
|
|
1446
|
+
const member = options.member ? resolveMember(ctx.db, options.member) : void 0;
|
|
1447
|
+
const fetchN = Math.max(top * 3, top + 20);
|
|
1448
|
+
let result;
|
|
1449
|
+
try {
|
|
1450
|
+
result = computeWordFrequency(ctx.db, {
|
|
1451
|
+
sessionId: ctx.session.id,
|
|
1452
|
+
locale: ctx.locale,
|
|
1453
|
+
timeFilter: toTimeFilter(time),
|
|
1454
|
+
memberId: member?.id,
|
|
1455
|
+
topN: fetchN,
|
|
1456
|
+
excludeKeywords: ctx.preprocessConfig.blacklistKeywords
|
|
1457
|
+
});
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1460
|
+
if (/jieba|segment/i.test(message)) {
|
|
1461
|
+
return {
|
|
1462
|
+
data: { items: [], status: "segmentation_unavailable" },
|
|
1463
|
+
meta: {
|
|
1464
|
+
session: ctx.session,
|
|
1465
|
+
hint: "Word segmentation engine unavailable (optional dependency @node-rs/jieba failed to load)"
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
throw err;
|
|
1470
|
+
}
|
|
1471
|
+
const enabledRules2 = ctx.preprocessConfig.desensitize ? ctx.preprocessConfig.desensitizeRules.filter((r) => r.enabled) : [];
|
|
1472
|
+
const items = result.words.filter((w) => enabledRules2.length === 0 || desensitizeText(w.word, enabledRules2) === w.word).slice(0, top);
|
|
1473
|
+
return {
|
|
1474
|
+
data: { items },
|
|
1475
|
+
meta: {
|
|
1476
|
+
session: ctx.session,
|
|
1477
|
+
timeRange: time.meta,
|
|
1478
|
+
...member ? { member } : {},
|
|
1479
|
+
totalMessages: result.totalMessages,
|
|
1480
|
+
uniqueWords: result.uniqueWords,
|
|
1481
|
+
returned: items.length,
|
|
1482
|
+
...items.length === 0 && result.totalMessages > 0 ? {
|
|
1483
|
+
hint: "No words extracted; the zh-CN segmentation dictionary may be missing (downloadable in the ChatLab app)"
|
|
1484
|
+
} : {}
|
|
1485
|
+
},
|
|
1486
|
+
renderText: () => items.map((w, i) => `${i + 1}. ${w.word} (${w.count})`).join("\n") || "No words."
|
|
1487
|
+
};
|
|
1488
|
+
} finally {
|
|
1489
|
+
ctx.close();
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
const responseCmd = statsCmd.command("response").description("Reply speed ranking (median response gap)");
|
|
1494
|
+
responseCmd.option("--session <ref>", "Session id or unique name").option("--last <dur>", "Relative window: <N>h|d|w (default 30d)").option("--top <n>", "Top N responders (default 10, max 100)").option("--format <format>", "Output format: agent|json|text");
|
|
1495
|
+
responseCmd.action(async (options) => {
|
|
1496
|
+
await runQuery("stats.response", options, async () => {
|
|
1497
|
+
const ctx = createQueryContext(options);
|
|
1498
|
+
try {
|
|
1499
|
+
const time = parseTimeOptions({ last: options.last ?? "30d" });
|
|
1500
|
+
const top = parseLimit(options.top, 10, 100, "--top");
|
|
1501
|
+
const rows = executeParameterizedSql(
|
|
1502
|
+
ctx.db,
|
|
1503
|
+
responseStatsMessagesSql(),
|
|
1504
|
+
{ startTs: time.startTs ?? 0 }
|
|
1505
|
+
);
|
|
1506
|
+
const items = computeResponseTimeStats(
|
|
1507
|
+
rows.map((r) => ({ senderId: r.sender_id, name: r.name, ts: r.ts })),
|
|
1508
|
+
top
|
|
1509
|
+
);
|
|
1510
|
+
return {
|
|
1511
|
+
data: { items },
|
|
1512
|
+
meta: { session: ctx.session, timeRange: time.meta, returned: items.length },
|
|
1513
|
+
renderText: () => items.map(
|
|
1514
|
+
(s, i) => `${i + 1}. ${s.name} - median ${s.medianSeconds}s, avg ${s.avgSeconds}s (${s.responseCount})`
|
|
1515
|
+
).join("\n") || "Not enough response data."
|
|
1516
|
+
};
|
|
1517
|
+
} finally {
|
|
1518
|
+
ctx.close();
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
function legacyStats(sessionId, options) {
|
|
1524
|
+
const { dbManager } = initRuntime();
|
|
1525
|
+
const db = dbManager.open(sessionId);
|
|
1526
|
+
if (!db) {
|
|
1527
|
+
console.error(`Session ${sessionId} not found`);
|
|
1528
|
+
process.exit(1);
|
|
1529
|
+
}
|
|
1530
|
+
const meta = getSessionMeta(db);
|
|
1531
|
+
const overview = getSessionOverview(db);
|
|
1532
|
+
const topMembers = getMemberActivity(db).slice(0, parseInt(options.top));
|
|
1533
|
+
if (options.format === "json") {
|
|
1534
|
+
console.log(JSON.stringify({ meta, overview, topMembers }, null, 2));
|
|
1535
|
+
} else {
|
|
1536
|
+
console.log(`
|
|
1537
|
+
Session: ${meta?.name}`);
|
|
1538
|
+
console.log(`Platform: ${meta?.platform} | Type: ${meta?.type}`);
|
|
1539
|
+
console.log(`Total messages: ${overview.totalMessages}`);
|
|
1540
|
+
console.log(`Total members: ${overview.totalMembers}`);
|
|
1541
|
+
const range = overview.firstMessageTs && overview.lastMessageTs ? `${new Date(overview.firstMessageTs * 1e3).toLocaleDateString()} ~ ${new Date(overview.lastMessageTs * 1e3).toLocaleDateString()}` : "Unknown";
|
|
1542
|
+
console.log(`Time range: ${range}`);
|
|
1543
|
+
if (topMembers.length > 0) {
|
|
1544
|
+
console.log(`
|
|
1545
|
+
Activity ranking (Top ${options.top}):`);
|
|
1546
|
+
for (const [i, m] of topMembers.entries()) {
|
|
1547
|
+
console.log(` ${i + 1}. ${m.name} - ${m.messageCount} messages (${m.percentage}%)`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
dbManager.closeAll();
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// src/query/commands-topics-sql.ts
|
|
1555
|
+
function enabledRules(ctx) {
|
|
1556
|
+
return ctx.preprocessConfig.desensitize ? ctx.preprocessConfig.desensitizeRules.filter((r) => r.enabled) : [];
|
|
1557
|
+
}
|
|
1558
|
+
function parseSqlRowLimit(value) {
|
|
1559
|
+
return parseLimit(value, 100, 1e3, "--limit", 1);
|
|
1560
|
+
}
|
|
1561
|
+
function clampSqlRows(rows, maxRows, alreadyTruncated) {
|
|
1562
|
+
if (maxRows <= 0 || rows.length <= maxRows) {
|
|
1563
|
+
return { rows, truncated: alreadyTruncated };
|
|
1564
|
+
}
|
|
1565
|
+
return { rows: rows.slice(0, maxRows), truncated: true };
|
|
1566
|
+
}
|
|
1567
|
+
function escapeRegExp(value) {
|
|
1568
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1569
|
+
}
|
|
1570
|
+
var SQL_IDENTIFIER = String.raw`(?:"[^"]+"|'[^']+'|` + "`[^`]+`" + String.raw`|\[[^\]]+\]|[A-Za-z_][A-Za-z0-9_]*)`;
|
|
1571
|
+
var SQL_TABLE_REF_END = String.raw`(?=\s|$|[,;])`;
|
|
1572
|
+
var SQL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1573
|
+
"where",
|
|
1574
|
+
"join",
|
|
1575
|
+
"left",
|
|
1576
|
+
"right",
|
|
1577
|
+
"inner",
|
|
1578
|
+
"outer",
|
|
1579
|
+
"cross",
|
|
1580
|
+
"order",
|
|
1581
|
+
"group",
|
|
1582
|
+
"limit",
|
|
1583
|
+
"on",
|
|
1584
|
+
"using"
|
|
1585
|
+
]);
|
|
1586
|
+
function sqlNamePattern(name) {
|
|
1587
|
+
const escaped = escapeRegExp(name);
|
|
1588
|
+
return String.raw`(?:"${escaped}"|'${escaped}'|` + "`" + escaped + "`" + String.raw`|\[${escaped}\]|${escaped})`;
|
|
1589
|
+
}
|
|
1590
|
+
function unquoteSqlIdentifier(value) {
|
|
1591
|
+
const trimmed = value.trim();
|
|
1592
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed.slice(1, -1);
|
|
1593
|
+
const quote = trimmed[0];
|
|
1594
|
+
if ((quote === '"' || quote === "'" || quote === "`") && trimmed.endsWith(quote)) {
|
|
1595
|
+
return trimmed.slice(1, -1);
|
|
1596
|
+
}
|
|
1597
|
+
return trimmed;
|
|
1598
|
+
}
|
|
1599
|
+
function messageTablePattern() {
|
|
1600
|
+
const schemaPrefix = String.raw`(?:${SQL_IDENTIFIER}\s*\.\s*)?`;
|
|
1601
|
+
return `${schemaPrefix}${sqlNamePattern("message")}`;
|
|
1602
|
+
}
|
|
1603
|
+
function selectsMessageStar(query) {
|
|
1604
|
+
const selectMatch = query.match(/\bselect\b([\s\S]+?)\bfrom\b/i);
|
|
1605
|
+
if (!selectMatch) return false;
|
|
1606
|
+
const projection = selectMatch[1];
|
|
1607
|
+
const messageTable = messageTablePattern();
|
|
1608
|
+
if (/(^|[,\s])\*(?=$|[,\s])/i.test(projection)) {
|
|
1609
|
+
return new RegExp(String.raw`\b(?:from|join)\s+${messageTable}${SQL_TABLE_REF_END}`, "i").test(query);
|
|
1610
|
+
}
|
|
1611
|
+
const aliasMatches = query.matchAll(
|
|
1612
|
+
new RegExp(String.raw`\b(?:from|join)\s+${messageTable}(?:\s+(?:as\s+)?(${SQL_IDENTIFIER}))?`, "gi")
|
|
1613
|
+
);
|
|
1614
|
+
const aliases = /* @__PURE__ */ new Set(["message"]);
|
|
1615
|
+
for (const match of aliasMatches) {
|
|
1616
|
+
const alias = match[1] ? unquoteSqlIdentifier(match[1]).toLowerCase() : void 0;
|
|
1617
|
+
if (alias && !SQL_KEYWORDS.has(alias)) aliases.add(alias);
|
|
1618
|
+
}
|
|
1619
|
+
return [...aliases].some(
|
|
1620
|
+
(alias) => new RegExp(`${sqlNamePattern(alias)}\\s*\\.\\s*\\*(?=$|[\\s,])`, "i").test(projection)
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
function assertSqlPrivacyAllowed(query, raw) {
|
|
1624
|
+
if (raw) return;
|
|
1625
|
+
if (!/(^|[^A-Za-z0-9_])["'`[]?content["'`\]]?(?![A-Za-z0-9_])/i.test(query) && !selectsMessageStar(query)) {
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
throw new QueryError({
|
|
1629
|
+
code: "INVALID_ARGUMENT",
|
|
1630
|
+
message: "SQL queries that read message content require --raw",
|
|
1631
|
+
hint: "Use dedicated message commands for privacy-processed content, or enable raw mode and pass --raw for debugging"
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
function assertSqlResultPrivacyAllowed(columns, raw) {
|
|
1635
|
+
if (raw) return;
|
|
1636
|
+
if (!columns.some((column) => unquoteSqlIdentifier(column).toLowerCase() === "content")) return;
|
|
1637
|
+
throw new QueryError({
|
|
1638
|
+
code: "INVALID_ARGUMENT",
|
|
1639
|
+
message: "SQL results that include message content require --raw",
|
|
1640
|
+
hint: "Use dedicated message commands for privacy-processed content, or enable raw mode and pass --raw for debugging"
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
function topicsAgentText(items) {
|
|
1644
|
+
if (items.length === 0) return "No summaries.";
|
|
1645
|
+
const blocks = items.map((t) => {
|
|
1646
|
+
const header = `[#${t.id}] ${t.since} ~ ${t.until} (${t.messages} msgs) ${t.participants.join(", ")}`;
|
|
1647
|
+
return t.summary ? `${header}
|
|
1648
|
+
${t.summary}` : header;
|
|
1649
|
+
});
|
|
1650
|
+
return `returned: ${items.length}
|
|
1651
|
+
|
|
1652
|
+
${blocks.join("\n\n")}`;
|
|
1653
|
+
}
|
|
1654
|
+
function registerTopicCommands(program2) {
|
|
1655
|
+
const topicsCmd = program2.command("topics").description("Conversation segments and AI summaries");
|
|
1656
|
+
topicsCmd.command("list").description("List AI-generated segment summaries (desensitized)").option("--session <ref>", "Session id or unique name").option("--since <t>", "Start time").option("--until <t>", "End time").option("--last <dur>", "Relative window: <N>h|d|w").option("--query <kw>", "Filter summaries by substring").option("--limit <n>", "Max summaries (default 20, max 100)").option("--format <format>", "Output format: agent|json|text").action(
|
|
1657
|
+
async (options) => {
|
|
1658
|
+
await runQuery("topics.list", options, async (format) => {
|
|
1659
|
+
const ctx = createQueryContext(options);
|
|
1660
|
+
try {
|
|
1661
|
+
const time = parseTimeOptions(options);
|
|
1662
|
+
const limit = parseLimit(options.limit, 20, 100, "--limit");
|
|
1663
|
+
const timeFilter = time.startTs !== void 0 || time.endTs !== void 0 ? { startTs: time.startTs ?? 0, endTs: time.endTs ?? Math.floor(Date.now() / 1e3) } : void 0;
|
|
1664
|
+
const rows = getSegmentSummaries(ctx.db, { limit: limit + 20, timeFilter });
|
|
1665
|
+
const rules = enabledRules(ctx);
|
|
1666
|
+
const blacklist = ctx.preprocessConfig.blacklistKeywords;
|
|
1667
|
+
let removed = 0;
|
|
1668
|
+
const items = rows.filter((row) => {
|
|
1669
|
+
if (row.summary && matchesBlacklist(row.summary, blacklist)) {
|
|
1670
|
+
removed++;
|
|
1671
|
+
return false;
|
|
1672
|
+
}
|
|
1673
|
+
return true;
|
|
1674
|
+
}).map((row) => ({
|
|
1675
|
+
id: row.id,
|
|
1676
|
+
since: epochToIso(row.startTs),
|
|
1677
|
+
until: epochToIso(row.endTs),
|
|
1678
|
+
messages: row.messageCount,
|
|
1679
|
+
participants: row.participants,
|
|
1680
|
+
summary: row.summary ? desensitizeText(row.summary, rules) : row.summary
|
|
1681
|
+
})).filter((row) => options.query ? (row.summary ?? "").includes(options.query) : true).slice(0, limit);
|
|
1682
|
+
if (items.length === 0) {
|
|
1683
|
+
const any = getSegmentSummaries(ctx.db, { limit: 1 });
|
|
1684
|
+
if (any.length === 0) {
|
|
1685
|
+
const hint = "Run summary generation in the ChatLab app, or use `chatlab stats keywords` as a fallback.";
|
|
1686
|
+
return {
|
|
1687
|
+
data: format === "agent" ? { text: "No AI summaries generated yet.", status: "summaries_not_generated" } : { items: [], status: "summaries_not_generated" },
|
|
1688
|
+
meta: { session: ctx.session, hint }
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
const meta = {
|
|
1693
|
+
session: ctx.session,
|
|
1694
|
+
timeRange: time.meta,
|
|
1695
|
+
returned: items.length,
|
|
1696
|
+
...removed > 0 ? { warnings: [`${removed} summaries removed by blacklist`] } : {}
|
|
1697
|
+
};
|
|
1698
|
+
return {
|
|
1699
|
+
data: format === "agent" ? { text: topicsAgentText(items) } : { items },
|
|
1700
|
+
meta,
|
|
1701
|
+
renderText: () => topicsAgentText(items)
|
|
1702
|
+
};
|
|
1703
|
+
} finally {
|
|
1704
|
+
ctx.close();
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
);
|
|
1709
|
+
topicsCmd.command("show").description("Show original messages of one segment").requiredOption("--id <segment-id>", "Segment id from topics list").option("--session <ref>", "Session id or unique name").option("--limit <n>", "Max messages (default 200, max 500)").option("--format <format>", "Output format: agent|json|text").option("--max-tokens <n>", "Token budget for agent text (default 4000)").option("--max-chars <n>", "Per-message content char limit").option("--full", "Disable per-message content truncation").option("--fields <a,b>", "json format: only include these item fields").option("--no-content", "json format: omit message content").option("--raw", "Bypass privacy preprocessing (debugging; requires user opt-in, json/text only)").option("--verbose", "Include preprocessing diagnostics in meta").action(
|
|
1710
|
+
async (options) => {
|
|
1711
|
+
await runQuery("topics.show", options, async (format) => {
|
|
1712
|
+
assertRawFormatCompatible(format, options);
|
|
1713
|
+
const segmentId = Number(options.id);
|
|
1714
|
+
if (!Number.isInteger(segmentId) || segmentId < 0) {
|
|
1715
|
+
throw new QueryError({
|
|
1716
|
+
code: "INVALID_ARGUMENT",
|
|
1717
|
+
message: `Invalid --id value: ${options.id}`,
|
|
1718
|
+
hint: "Segment ids come from `chatlab topics list`"
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
const ctx = createQueryContext(options);
|
|
1722
|
+
try {
|
|
1723
|
+
assertRawAllowed(ctx, options);
|
|
1724
|
+
const limit = parseLimit(options.limit, 200, 500, "--limit");
|
|
1725
|
+
const segment = getSegmentMessages(ctx.db, segmentId, limit);
|
|
1726
|
+
if (!segment) {
|
|
1727
|
+
throw new QueryError({
|
|
1728
|
+
code: "SEGMENT_NOT_FOUND",
|
|
1729
|
+
message: `Segment ${segmentId} not found`,
|
|
1730
|
+
hint: "Run `chatlab topics list` to see available segments"
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
const meta = {
|
|
1734
|
+
session: ctx.session,
|
|
1735
|
+
segment: {
|
|
1736
|
+
id: segment.segmentId,
|
|
1737
|
+
since: epochToIso(segment.startTs),
|
|
1738
|
+
until: epochToIso(segment.endTs),
|
|
1739
|
+
messageCount: segment.messageCount,
|
|
1740
|
+
participants: segment.participants
|
|
1741
|
+
},
|
|
1742
|
+
returnedHits: segment.returnedCount
|
|
1743
|
+
};
|
|
1744
|
+
return buildMessagesResult(format, ctx, options, segment.messages, meta, {
|
|
1745
|
+
strategy: "keep_last"
|
|
1746
|
+
});
|
|
1747
|
+
} finally {
|
|
1748
|
+
ctx.close();
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
function registerSqlCommands(program2) {
|
|
1755
|
+
program2.command("sql").description("Read-only SQL fallback (string cells are desensitized; message content requires --raw)").argument("<query>", "SELECT / WITH statement").option("--session <ref>", "Session id or unique name").option("--limit <n>", "Max rows (default 100, max 1000)").option("--format <format>", "Output format: agent|json|text").option("--raw", "Return raw cell values (debugging; requires user opt-in)").action(async (query, options) => {
|
|
1756
|
+
await runQuery("sql", options, async () => {
|
|
1757
|
+
const ctx = createQueryContext(options);
|
|
1758
|
+
try {
|
|
1759
|
+
if (!ctx.allowSql) {
|
|
1760
|
+
throw new QueryError({
|
|
1761
|
+
code: "SQL_DISABLED",
|
|
1762
|
+
message: "The sql command is disabled by configuration",
|
|
1763
|
+
hint: "The user can re-enable it with `chatlab config set cli.allow_sql true`"
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
assertRawAllowed(ctx, options);
|
|
1767
|
+
assertSqlPrivacyAllowed(query, !!options.raw);
|
|
1768
|
+
const limit = parseSqlRowLimit(options.limit);
|
|
1769
|
+
let result;
|
|
1770
|
+
try {
|
|
1771
|
+
result = executeReadonlySql(ctx.db, query, limit);
|
|
1772
|
+
} catch (err) {
|
|
1773
|
+
throw new QueryError({
|
|
1774
|
+
code: "SQL_ERROR",
|
|
1775
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1776
|
+
hint: "Run `chatlab schema` to inspect available tables and columns"
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
assertSqlResultPrivacyAllowed(result.columns, !!options.raw);
|
|
1780
|
+
const clamped = clampSqlRows(result.rows, limit, result.truncated);
|
|
1781
|
+
let rows = clamped.rows;
|
|
1782
|
+
let removed = 0;
|
|
1783
|
+
if (!options.raw) {
|
|
1784
|
+
const rules = enabledRules(ctx);
|
|
1785
|
+
const blacklist = ctx.preprocessConfig.blacklistKeywords;
|
|
1786
|
+
const sanitized = [];
|
|
1787
|
+
for (const row of rows) {
|
|
1788
|
+
let drop = false;
|
|
1789
|
+
const clean = {};
|
|
1790
|
+
for (const [key, value] of Object.entries(row)) {
|
|
1791
|
+
if (typeof value === "string") {
|
|
1792
|
+
if (matchesBlacklist(value, blacklist)) {
|
|
1793
|
+
drop = true;
|
|
1794
|
+
break;
|
|
1795
|
+
}
|
|
1796
|
+
clean[key] = desensitizeText(value, rules);
|
|
1797
|
+
} else {
|
|
1798
|
+
clean[key] = value;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if (drop) removed++;
|
|
1802
|
+
else sanitized.push(clean);
|
|
1803
|
+
}
|
|
1804
|
+
rows = sanitized;
|
|
1805
|
+
}
|
|
1806
|
+
return {
|
|
1807
|
+
data: { columns: result.columns, rows, rowCount: rows.length, truncated: clamped.truncated },
|
|
1808
|
+
meta: {
|
|
1809
|
+
session: ctx.session,
|
|
1810
|
+
...options.raw ? { preprocess: { raw: true } } : {},
|
|
1811
|
+
...removed > 0 ? { warnings: [`${removed} row(s) removed by blacklist`] } : {}
|
|
1812
|
+
},
|
|
1813
|
+
renderText: () => rows.length === 0 ? "No results." : rows.map((r) => JSON.stringify(r)).join("\n") + `
|
|
1814
|
+
${rows.length} row(s)${clamped.truncated ? " (truncated)" : ""}`
|
|
1815
|
+
};
|
|
1816
|
+
} finally {
|
|
1817
|
+
ctx.close();
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
});
|
|
1821
|
+
program2.command("schema").description("Show session database schema (for the sql command)").option("--session <ref>", "Session id or unique name").option("--format <format>", "Output format: agent|json|text").action(async (options) => {
|
|
1822
|
+
await runQuery("schema", options, async () => {
|
|
1823
|
+
const ctx = createQueryContext(options);
|
|
1824
|
+
try {
|
|
1825
|
+
const items = getDatabaseSchema(ctx.db);
|
|
1826
|
+
return {
|
|
1827
|
+
data: { items },
|
|
1828
|
+
meta: { session: ctx.session, returned: items.length },
|
|
1829
|
+
renderText: () => items.map((t) => t.sql).join("\n\n")
|
|
1830
|
+
};
|
|
1831
|
+
} finally {
|
|
1832
|
+
ctx.close();
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// src/query/register.ts
|
|
1839
|
+
function registerQueryCommands(program2) {
|
|
1840
|
+
registerSessionCommands(program2);
|
|
1841
|
+
registerMemberCommands(program2);
|
|
1842
|
+
registerMessageCommands(program2);
|
|
1843
|
+
registerStatsCommands(program2);
|
|
1844
|
+
registerTopicCommands(program2);
|
|
1845
|
+
registerSqlCommands(program2);
|
|
1846
|
+
registerLegacyAliases(program2);
|
|
1847
|
+
}
|
|
1848
|
+
function assertLegacySqlAllowed(config, sql) {
|
|
1849
|
+
if (!config.cli.allow_sql) {
|
|
1850
|
+
throw new QueryError({
|
|
1851
|
+
code: "SQL_DISABLED",
|
|
1852
|
+
message: "The sql command is disabled by configuration",
|
|
1853
|
+
hint: "The user can re-enable it with `chatlab config set cli.allow_sql true`"
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
if (sql) assertSqlPrivacyAllowed(sql, false);
|
|
1857
|
+
}
|
|
1858
|
+
function registerLegacyAliases(program2) {
|
|
1859
|
+
program2.command("search <session-id> <keyword>", { hidden: true }).description("Deprecated: use `chatlab messages search`").option("--limit <n>", "Max results to return", "20").option("--format <format>", "Output format (table|json)", "table").action((sessionId, keyword, options) => {
|
|
1860
|
+
console.error(
|
|
1861
|
+
"[Deprecated] `chatlab search` \u2014 use `chatlab messages search <keyword> --session <id>`; alias kept for one major version"
|
|
1862
|
+
);
|
|
1863
|
+
const { dbManager } = initRuntime();
|
|
1864
|
+
const db = dbManager.open(sessionId);
|
|
1865
|
+
if (!db) {
|
|
1866
|
+
console.error(`Session ${sessionId} not found`);
|
|
1867
|
+
process.exit(1);
|
|
1868
|
+
}
|
|
1869
|
+
const limit = parseInt(options.limit);
|
|
1870
|
+
const result = searchMessagesLike(db, keyword, { limit });
|
|
1871
|
+
if (options.format === "json") {
|
|
1872
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1873
|
+
} else {
|
|
1874
|
+
console.log(
|
|
1875
|
+
`Search "${keyword}" - ${result.total} result(s)${result.hasMore ? " (showing first " + limit + ")" : ""}:
|
|
1876
|
+
`
|
|
1877
|
+
);
|
|
1878
|
+
for (const msg of result.messages) {
|
|
1879
|
+
const time = new Date(msg.timestamp * 1e3).toLocaleString();
|
|
1880
|
+
console.log(` [${time}] ${msg.senderName}: ${msg.content}`);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
dbManager.closeAll();
|
|
1884
|
+
});
|
|
1885
|
+
program2.command("query <session-id>", { hidden: true }).description("Deprecated: use `chatlab sql`").requiredOption("--sql <sql>", "SQL query statement").option("--format <format>", "Output format (table|json)", "table").action((sessionId, options) => {
|
|
1886
|
+
console.error(
|
|
1887
|
+
'[Deprecated] `chatlab query` \u2014 use `chatlab sql "<SELECT ...>" --session <id>`; alias kept for one major version'
|
|
1888
|
+
);
|
|
1889
|
+
const { config, dbManager } = initRuntime();
|
|
1890
|
+
const db = dbManager.open(sessionId);
|
|
1891
|
+
if (!db) {
|
|
1892
|
+
console.error(`Session ${sessionId} not found`);
|
|
1893
|
+
process.exit(1);
|
|
1894
|
+
}
|
|
1895
|
+
try {
|
|
1896
|
+
assertLegacySqlAllowed(config, options.sql);
|
|
1897
|
+
const result = executeReadonlySql(db, options.sql);
|
|
1898
|
+
assertSqlResultPrivacyAllowed(result.columns, false);
|
|
1899
|
+
if (options.format === "json") {
|
|
1900
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1901
|
+
} else {
|
|
1902
|
+
if (result.rows.length === 0) {
|
|
1903
|
+
console.log("No results.");
|
|
1904
|
+
} else {
|
|
1905
|
+
printTable(result.columns, result.rows);
|
|
1906
|
+
console.log(`
|
|
1907
|
+
${result.rowCount} row(s)${result.truncated ? " (truncated)" : ""}`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
} catch (err) {
|
|
1911
|
+
if (err instanceof QueryError) {
|
|
1912
|
+
console.error(`${err.code}: ${err.message}`);
|
|
1913
|
+
if (err.hint) console.error(`Hint: ${err.hint}`);
|
|
1914
|
+
process.exit(2);
|
|
1915
|
+
}
|
|
1916
|
+
console.error(`SQL error: ${err instanceof Error ? err.message : err}`);
|
|
1917
|
+
process.exit(1);
|
|
1918
|
+
}
|
|
1919
|
+
dbManager.closeAll();
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
function printTable(columns, rows) {
|
|
1923
|
+
const widths = columns.map((col) => {
|
|
1924
|
+
const maxData = rows.reduce((max, row) => {
|
|
1925
|
+
const val = String(row[col] ?? "");
|
|
1926
|
+
return Math.max(max, val.length);
|
|
1927
|
+
}, 0);
|
|
1928
|
+
return Math.max(col.length, Math.min(maxData, 40));
|
|
1929
|
+
});
|
|
1930
|
+
const header = columns.map((col, i) => col.padEnd(widths[i])).join(" | ");
|
|
1931
|
+
const separator = widths.map((w) => "-".repeat(w)).join("-+-");
|
|
1932
|
+
console.log(header);
|
|
1933
|
+
console.log(separator);
|
|
1934
|
+
for (const row of rows) {
|
|
1935
|
+
const line = columns.map((col, i) => {
|
|
1936
|
+
const val = String(row[col] ?? "");
|
|
1937
|
+
return val.length > 40 ? val.slice(0, 37) + "..." : val.padEnd(widths[i]);
|
|
1938
|
+
}).join(" | ");
|
|
1939
|
+
console.log(line);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// src/query/manifest.ts
|
|
1944
|
+
var QUERY_GROUPS = /* @__PURE__ */ new Set(["sessions", "members", "messages", "stats", "topics", "sql", "schema", "manifest"]);
|
|
1945
|
+
var EXIT_CODES = {
|
|
1946
|
+
"0": "success",
|
|
1947
|
+
"1": "internal error",
|
|
1948
|
+
"2": "invalid argument, invalid cursor, or disabled capability",
|
|
1949
|
+
"3": "resource not found (session/member/message/segment)",
|
|
1950
|
+
"4": "ambiguous reference (see error.candidates)",
|
|
1951
|
+
"5": "SQL error"
|
|
1952
|
+
};
|
|
1953
|
+
var EXAMPLES = [
|
|
1954
|
+
{ task: "What did this group talk about today?", command: "chatlab messages list --since today --format agent" },
|
|
1955
|
+
{
|
|
1956
|
+
task: "Search a topic within a time range",
|
|
1957
|
+
command: "chatlab messages search \u65C5\u6E38 --since 2026-01-01 --limit 20 --format agent"
|
|
1958
|
+
},
|
|
1959
|
+
{
|
|
1960
|
+
task: "Who mentioned it first (message ids)?",
|
|
1961
|
+
command: "chatlab messages search \u62A5\u9500 --sort asc --limit 5 --format agent"
|
|
1962
|
+
},
|
|
1963
|
+
{
|
|
1964
|
+
task: "Follow up a single [#id] marker with context",
|
|
1965
|
+
command: "chatlab messages context --id 1021 --window 10 --format agent"
|
|
1966
|
+
},
|
|
1967
|
+
{
|
|
1968
|
+
task: "Conversation between me and a member last month",
|
|
1969
|
+
command: "chatlab messages between --member me --member \u5C0F\u7EA2 --last 30d --format agent"
|
|
1970
|
+
},
|
|
1971
|
+
{ task: "Most active members", command: "chatlab stats activity --top 10 --format json" },
|
|
1972
|
+
{ task: "Monthly hot topics", command: "chatlab topics list --since 2026-06-01 --until 2026-06-30 --format agent" },
|
|
1973
|
+
{ task: "High-frequency words", command: "chatlab stats keywords --last 30d --top 20 --format json" },
|
|
1974
|
+
{
|
|
1975
|
+
task: "Structural scouting without content",
|
|
1976
|
+
command: "chatlab messages list --last 7d --no-content --fields id,senderName,time --format json"
|
|
1977
|
+
},
|
|
1978
|
+
{ task: "Discover sessions first", command: "chatlab sessions list --format json" }
|
|
1979
|
+
];
|
|
1980
|
+
var NOTES = [
|
|
1981
|
+
"Always pass --format explicitly: agent for message bodies, json for structural data.",
|
|
1982
|
+
"stdout carries exactly one JSON envelope in agent/json mode; logs go to stderr.",
|
|
1983
|
+
"Success envelope: { ok, command, data, meta }; failure: { ok: false, command, error }.",
|
|
1984
|
+
"Pagination: follow meta.nextCursor with --cursor; cursors are bound to the exact query.",
|
|
1985
|
+
"Single-message [#id] and [#id*] markers are usable with `messages context --id`; merged [#a-b] ranges are display-only.",
|
|
1986
|
+
"Privacy preprocessing (desensitize/blacklist) is always on by default."
|
|
1987
|
+
];
|
|
1988
|
+
function isHidden(cmd) {
|
|
1989
|
+
return cmd._hidden === true;
|
|
1990
|
+
}
|
|
1991
|
+
function serializeCommand(cmd, prefix) {
|
|
1992
|
+
const args = (cmd.registeredArguments ?? []).map((arg) => ({
|
|
1993
|
+
name: arg.name(),
|
|
1994
|
+
required: arg.required,
|
|
1995
|
+
variadic: arg.variadic,
|
|
1996
|
+
...arg.description ? { description: arg.description } : {}
|
|
1997
|
+
}));
|
|
1998
|
+
const options = cmd.options.filter((opt) => !opt.hidden).map((opt) => ({
|
|
1999
|
+
flags: opt.flags,
|
|
2000
|
+
description: opt.description,
|
|
2001
|
+
...opt.defaultValue !== void 0 ? { default: opt.defaultValue } : {}
|
|
2002
|
+
}));
|
|
2003
|
+
return {
|
|
2004
|
+
name: prefix ? `${prefix} ${cmd.name()}` : cmd.name(),
|
|
2005
|
+
description: cmd.description(),
|
|
2006
|
+
args,
|
|
2007
|
+
options
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
function collectQueryCommands(cmd, prefix, into) {
|
|
2011
|
+
if (cmd.commands.length === 0) {
|
|
2012
|
+
into.push(serializeCommand(cmd, prefix));
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
const path2 = prefix ? `${prefix} ${cmd.name()}` : cmd.name();
|
|
2016
|
+
for (const sub of cmd.commands) {
|
|
2017
|
+
if (isHidden(sub)) continue;
|
|
2018
|
+
collectQueryCommands(sub, path2, into);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
function buildManifest(program2, version) {
|
|
2022
|
+
const commands = [];
|
|
2023
|
+
const appCommands = [];
|
|
2024
|
+
for (const cmd of program2.commands) {
|
|
2025
|
+
if (isHidden(cmd)) continue;
|
|
2026
|
+
if (QUERY_GROUPS.has(cmd.name())) {
|
|
2027
|
+
collectQueryCommands(cmd, "", commands);
|
|
2028
|
+
} else {
|
|
2029
|
+
appCommands.push({ name: cmd.name(), description: cmd.description() });
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
return {
|
|
2033
|
+
name: "chatlab",
|
|
2034
|
+
version,
|
|
2035
|
+
apiVersion: API_VERSION,
|
|
2036
|
+
formats: ["agent", "json", "text"],
|
|
2037
|
+
commands,
|
|
2038
|
+
appCommands,
|
|
2039
|
+
exitCodes: EXIT_CODES,
|
|
2040
|
+
examples: EXAMPLES,
|
|
2041
|
+
notes: NOTES
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
function registerManifestCommand(program2, version) {
|
|
2045
|
+
program2.command("manifest").description("Machine-readable command manifest for AI agents (JSON)").action(() => {
|
|
2046
|
+
console.log(JSON.stringify(buildManifest(program2, version), null, 2));
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// src/cli.ts
|
|
2051
|
+
var program = new Command();
|
|
2052
|
+
program.name("chatlab").description("ChatLab - Chat history analysis tool").version(getVersion(), "-v, --version");
|
|
2053
|
+
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
2054
|
+
if (actionCommand.name() === "update") return;
|
|
2055
|
+
const { checkForUpdatesInteractive } = await import("./update-checker-LGMTEKMT.mjs");
|
|
2056
|
+
await checkForUpdatesInteractive();
|
|
2057
|
+
});
|
|
2058
|
+
program.command("update").description("Update ChatLab CLI to the latest version").action(async () => {
|
|
2059
|
+
const { performCliSelfUpdate } = await import("./update-checker-LGMTEKMT.mjs");
|
|
2060
|
+
const result = await performCliSelfUpdate({
|
|
2061
|
+
write: (text) => process.stderr.write(text)
|
|
2062
|
+
});
|
|
2063
|
+
if (result.success) {
|
|
2064
|
+
console.error(" Updated successfully. Please restart chatlab to use the new version.\n");
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
console.error(` Update failed: ${result.error || "unknown error"}
|
|
2068
|
+
`);
|
|
2069
|
+
process.exitCode = 1;
|
|
2070
|
+
});
|
|
2071
|
+
registerQueryCommands(program);
|
|
2072
|
+
registerManifestCommand(program, getVersion());
|
|
2073
|
+
program.command("import <file>").description("Import a chat history file (14+ formats: QQ/WeChat/Telegram/WhatsApp/LINE/Discord/Instagram, etc.)").option("--session-id <id>", "Specify session ID (auto-generated if omitted)").option("--format <id>", "Specify format ID (skip auto-detection)").action(async (file, options) => {
|
|
2074
|
+
if (!fs2.existsSync(file)) {
|
|
2075
|
+
console.error(`File not found: ${file}`);
|
|
2076
|
+
process.exit(1);
|
|
2077
|
+
}
|
|
2078
|
+
const { streamImport, detectFormat } = await import("./import-WWCSXGVS.mjs");
|
|
2079
|
+
const { dbManager, pathProvider } = initRuntime();
|
|
2080
|
+
const nativeBinding = resolveNativeBinding();
|
|
2081
|
+
logNativeParserStatus();
|
|
2082
|
+
const format = detectFormat(file);
|
|
2083
|
+
if (!format && !options.format) {
|
|
2084
|
+
console.error(`Unrecognized file format: ${file}`);
|
|
2085
|
+
console.error('Use --format <id> to specify manually, or run "chatlab formats" to see supported formats');
|
|
2086
|
+
process.exit(1);
|
|
2087
|
+
}
|
|
2088
|
+
console.log(`Importing: ${file}`);
|
|
2089
|
+
if (format) console.log(` Format: ${format.name} (${format.platform})`);
|
|
2090
|
+
try {
|
|
2091
|
+
const result = await streamImport(dbManager, file, {
|
|
2092
|
+
formatId: options.format,
|
|
2093
|
+
nativeBinding,
|
|
2094
|
+
onProgress: (p) => {
|
|
2095
|
+
process.stdout.write(`\r ${p.stage}: ${p.progress}%`);
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
if (result.success) {
|
|
2099
|
+
console.log(`
|
|
2100
|
+
|
|
2101
|
+
Import succeeded!`);
|
|
2102
|
+
console.log(` Session ID: ${result.sessionId}`);
|
|
2103
|
+
console.log(` Messages written: ${result.diagnostics?.messagesWritten ?? 0}`);
|
|
2104
|
+
console.log(` Messages skipped: ${result.diagnostics?.messagesSkipped ?? 0}`);
|
|
2105
|
+
if (result.sessionId) {
|
|
2106
|
+
try {
|
|
2107
|
+
const { PreferencesManager: PreferencesManager2, createDatabaseManagerAdapter, ownerProfileService } = await import("./src-MPNBTFOE.mjs");
|
|
2108
|
+
const applied = ownerProfileService.tryApplyOwnerProfile(
|
|
2109
|
+
createDatabaseManagerAdapter(dbManager),
|
|
2110
|
+
new PreferencesManager2(pathProvider.getSystemDir()),
|
|
2111
|
+
result.sessionId
|
|
2112
|
+
);
|
|
2113
|
+
if (applied.applied) {
|
|
2114
|
+
console.log(` Owner auto-detected: ${applied.ownerId}`);
|
|
2115
|
+
}
|
|
2116
|
+
} catch (ownerErr) {
|
|
2117
|
+
console.warn(` Owner profile apply skipped: ${ownerErr instanceof Error ? ownerErr.message : ownerErr}`);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
} else {
|
|
2121
|
+
console.error(`
|
|
2122
|
+
|
|
2123
|
+
Import failed: ${result.error}`);
|
|
2124
|
+
process.exit(1);
|
|
2125
|
+
}
|
|
2126
|
+
} catch (err) {
|
|
2127
|
+
console.error(`
|
|
2128
|
+
|
|
2129
|
+
Import error: ${err instanceof Error ? err.message : err}`);
|
|
2130
|
+
process.exit(1);
|
|
2131
|
+
} finally {
|
|
2132
|
+
dbManager.closeAll();
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
program.command("formats").description("List all supported chat history formats").action(async () => {
|
|
2136
|
+
const { getSupportedFormats } = await import("./import-WWCSXGVS.mjs");
|
|
2137
|
+
const formats = getSupportedFormats();
|
|
2138
|
+
console.log(`${formats.length} supported format(s):
|
|
2139
|
+
`);
|
|
2140
|
+
for (const f of formats) {
|
|
2141
|
+
console.log(` ${f.id.padEnd(30)} ${f.name} (${f.platform}) [${f.extensions.join(", ")}]`);
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
program.command("mcp").description("Start MCP Server (stdio transport, for ClaudeCode / Cursor / AI agents)").action(async () => {
|
|
2145
|
+
const { startCliMcpServer } = await import("./mcp-A4M3LT5O.mjs");
|
|
2146
|
+
await startCliMcpServer();
|
|
2147
|
+
});
|
|
2148
|
+
program.command("chat").description("Ask ChatLab AI about an imported chat session").option("--session-id <id>", "Source chat session ID").option("--ai-chat-id <id>", "Existing AI chat ID to continue").option("-q, --question <text>", "Question to ask").option("--json", "Output structured JSON").option("--include-events", "Include all agent stream chunks in JSON output").option("--no-stream", "Disable streaming output").option("--locale <locale>", "AI response locale", "zh-CN").action(async (options) => {
|
|
2149
|
+
const { runChatCommand } = await import("./chat-command-VBDMWOKL.mjs");
|
|
2150
|
+
const { dbManager, pathProvider } = initRuntime();
|
|
2151
|
+
const aiChatManager = new AIChatManager(pathProvider.getAiDataDir(), { nativeBinding: resolveNativeBinding() });
|
|
2152
|
+
try {
|
|
2153
|
+
await runChatCommand(
|
|
2154
|
+
{
|
|
2155
|
+
sessionId: options.sessionId,
|
|
2156
|
+
aiChatId: options.aiChatId,
|
|
2157
|
+
question: options.question,
|
|
2158
|
+
json: !!options.json,
|
|
2159
|
+
stream: options.stream,
|
|
2160
|
+
locale: options.locale,
|
|
2161
|
+
includeEvents: !!options.includeEvents
|
|
2162
|
+
},
|
|
2163
|
+
{ dbManager, pathProvider, aiChatManager }
|
|
2164
|
+
);
|
|
2165
|
+
} catch (error) {
|
|
2166
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2167
|
+
process.exitCode = 1;
|
|
2168
|
+
} finally {
|
|
2169
|
+
aiChatManager.close();
|
|
2170
|
+
dbManager.closeAll();
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
program.command("start").description("Start ChatLab (HTTP API + Web UI)").option("--port <port>", "Server port", String(DEFAULT_API_PORT)).option("--host <host>", "Listen address", "127.0.0.1").option("--token <token>", "Custom Bearer Token (reads from config or auto-generates if omitted)").option("--headless", "API-only mode, do not serve the Web UI").option("--require-auth", "Require Bearer token for all routes including /_web/*").option("--no-open", "Do not auto-open the browser").option("--daemon", "Run as a resident system service (auto-start on login, macOS/Linux)").action(async (options) => {
|
|
2174
|
+
if (options.daemon) {
|
|
2175
|
+
const { serviceInstall } = await import("./service-KGL3RAIA.mjs");
|
|
2176
|
+
serviceInstall({
|
|
2177
|
+
port: parseInt(options.port, 10),
|
|
2178
|
+
host: options.host,
|
|
2179
|
+
token: options.token || void 0,
|
|
2180
|
+
headless: options.headless,
|
|
2181
|
+
requireAuth: options.requireAuth || void 0
|
|
2182
|
+
});
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const { startHttpServer } = await import("./http-L4AMELPZ.mjs");
|
|
2186
|
+
const port = parseInt(options.port, 10);
|
|
2187
|
+
let webRoot;
|
|
2188
|
+
if (!options.headless) {
|
|
2189
|
+
const webDir = resolveCliPath("dist-web");
|
|
2190
|
+
if (fs2.existsSync(webDir)) {
|
|
2191
|
+
webRoot = webDir;
|
|
2192
|
+
} else {
|
|
2193
|
+
console.warn("Warning: dist-web/ not found, starting in API-only mode");
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
try {
|
|
2197
|
+
if (!await isPortAvailable(port, options.host)) {
|
|
2198
|
+
console.error(formatPortInUseError(port));
|
|
2199
|
+
process.exit(1);
|
|
2200
|
+
}
|
|
2201
|
+
const info = await startHttpServer({
|
|
2202
|
+
port,
|
|
2203
|
+
host: options.host,
|
|
2204
|
+
token: options.token || void 0,
|
|
2205
|
+
webRoot,
|
|
2206
|
+
requireAuth: options.requireAuth || void 0
|
|
2207
|
+
});
|
|
2208
|
+
const { startPeriodicUpdateCheck } = await import("./update-checker-LGMTEKMT.mjs");
|
|
2209
|
+
startPeriodicUpdateCheck();
|
|
2210
|
+
const displayHost = info.host === "0.0.0.0" ? "127.0.0.1" : info.host;
|
|
2211
|
+
const url = `http://${displayHost}:${info.port}`;
|
|
2212
|
+
console.log(`
|
|
2213
|
+
ChatLab v${getVersion()}`);
|
|
2214
|
+
if (webRoot) console.log(` Web UI: ${url}/`);
|
|
2215
|
+
console.log(` API: ${url}`);
|
|
2216
|
+
console.log(` Token: ${info.token}`);
|
|
2217
|
+
console.log(`
|
|
2218
|
+
Example:`);
|
|
2219
|
+
console.log(` curl -H "Authorization: Bearer ${info.token}" ${url}/api/v1/status`);
|
|
2220
|
+
if (webRoot && options.open) {
|
|
2221
|
+
openBrowser(url);
|
|
2222
|
+
console.log(`
|
|
2223
|
+
Browser opened. Press Ctrl+C to stop.
|
|
2224
|
+
`);
|
|
2225
|
+
} else {
|
|
2226
|
+
console.log(`
|
|
2227
|
+
Press Ctrl+C to stop.
|
|
2228
|
+
`);
|
|
2229
|
+
}
|
|
2230
|
+
const shutdown = async () => {
|
|
2231
|
+
console.log("\nShutting down...");
|
|
2232
|
+
const { stopHttpServer } = await import("./http-L4AMELPZ.mjs");
|
|
2233
|
+
await stopHttpServer();
|
|
2234
|
+
process.exit(0);
|
|
2235
|
+
};
|
|
2236
|
+
process.on("SIGINT", shutdown);
|
|
2237
|
+
process.on("SIGTERM", shutdown);
|
|
2238
|
+
} catch (err) {
|
|
2239
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2240
|
+
if (message.includes("EADDRINUSE")) {
|
|
2241
|
+
console.error(formatPortInUseError(port));
|
|
2242
|
+
} else {
|
|
2243
|
+
console.error(`Startup failed: ${message}`);
|
|
2244
|
+
}
|
|
2245
|
+
process.exit(1);
|
|
2246
|
+
}
|
|
2247
|
+
});
|
|
2248
|
+
var configCmd = program.command("config").description("Configuration management");
|
|
2249
|
+
configCmd.command("path").description("Show config file path").action(() => {
|
|
2250
|
+
console.log(getConfigPath());
|
|
2251
|
+
});
|
|
2252
|
+
configCmd.command("show").description("Show current configuration").action(() => {
|
|
2253
|
+
const config = loadConfig();
|
|
2254
|
+
console.log(JSON.stringify(config, null, 2));
|
|
2255
|
+
});
|
|
2256
|
+
configCmd.command("set <key> <value>").description("Set a config field, e.g. `chatlab config set cli.allow_raw true`").action((key, value) => {
|
|
2257
|
+
try {
|
|
2258
|
+
const result = setConfigField(key, value);
|
|
2259
|
+
console.log(`${result.section}.${result.key} = ${JSON.stringify(result.value)}`);
|
|
2260
|
+
} catch (err) {
|
|
2261
|
+
if (err instanceof ConfigSetError) {
|
|
2262
|
+
console.error(`Error: ${err.message}`);
|
|
2263
|
+
process.exitCode = 2;
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
throw err;
|
|
2267
|
+
}
|
|
2268
|
+
});
|
|
2269
|
+
program.command("stop").description("Stop the resident service and remove auto-start (reverse of start --daemon)").action(async () => {
|
|
2270
|
+
const { serviceUninstall } = await import("./service-KGL3RAIA.mjs");
|
|
2271
|
+
serviceUninstall();
|
|
2272
|
+
});
|
|
2273
|
+
program.command("status").description("Show resident service status").action(async () => {
|
|
2274
|
+
const { getServiceStatus } = await import("./service-KGL3RAIA.mjs");
|
|
2275
|
+
const svc = getServiceStatus();
|
|
2276
|
+
console.log("\nChatLab Status");
|
|
2277
|
+
console.log("\u2500".repeat(36));
|
|
2278
|
+
console.log(` Logs: ${getSystemLogsDir()}`);
|
|
2279
|
+
if (svc.installed) {
|
|
2280
|
+
const portStr = svc.port ? `http://${svc.host ?? "127.0.0.1"}:${svc.port}` : "";
|
|
2281
|
+
console.log(` Service: ${svc.running ? "running" : "installed (not running)"}`);
|
|
2282
|
+
if (portStr) console.log(` Address: ${portStr}`);
|
|
2283
|
+
console.log(` Auto-start: enabled`);
|
|
2284
|
+
console.log(`
|
|
2285
|
+
Use \`chatlab stop\` to remove the service.
|
|
2286
|
+
`);
|
|
2287
|
+
} else {
|
|
2288
|
+
console.log(` Service: not installed`);
|
|
2289
|
+
console.log(` Auto-start: disabled`);
|
|
2290
|
+
console.log(`
|
|
2291
|
+
Use \`chatlab start --daemon\` to install as a system service.
|
|
2292
|
+
`);
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
function openBrowser(url) {
|
|
2296
|
+
try {
|
|
2297
|
+
const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
|
|
2298
|
+
execSync(cmd, { stdio: "ignore" });
|
|
2299
|
+
} catch {
|
|
2300
|
+
console.log(` Open manually: ${url}`);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
function run(argv) {
|
|
2304
|
+
initAppLogger(getSystemLogsDir());
|
|
2305
|
+
process.on("uncaughtException", (error) => {
|
|
2306
|
+
console.error("Uncaught Exception:", error);
|
|
2307
|
+
appLogger.error("crash", "uncaughtException", error);
|
|
2308
|
+
process.exit(1);
|
|
2309
|
+
});
|
|
2310
|
+
process.on("unhandledRejection", (reason) => {
|
|
2311
|
+
console.error("Unhandled Rejection:", reason);
|
|
2312
|
+
appLogger.error("crash", "unhandledRejection", reason);
|
|
2313
|
+
process.exit(1);
|
|
2314
|
+
});
|
|
2315
|
+
program.parse(argv);
|
|
2316
|
+
}
|
|
2317
|
+
var isDirectRun = process.argv[1]?.endsWith("cli.ts") || process.argv[1]?.endsWith("cli.js");
|
|
2318
|
+
if (isDirectRun) {
|
|
2319
|
+
run();
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
export {
|
|
2323
|
+
run
|
|
2324
|
+
};
|
|
2325
|
+
//# sourceMappingURL=chunk-M2PXDX4G.mjs.map
|