@mrc2204/agent-smart-memo 5.0.2 → 5.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +209 -375
  2. package/bin/asm.mjs +365 -0
  3. package/bin/opencode-mcp-server.mjs +320 -0
  4. package/dist/core/contracts/adapter-contracts.d.ts +1 -1
  5. package/dist/core/contracts/adapter-contracts.d.ts.map +1 -1
  6. package/dist/core/contracts/change-overlay-contracts.d.ts +69 -0
  7. package/dist/core/contracts/change-overlay-contracts.d.ts.map +1 -0
  8. package/dist/core/contracts/change-overlay-contracts.js +2 -0
  9. package/dist/core/contracts/change-overlay-contracts.js.map +1 -0
  10. package/dist/core/contracts/feature-pack-contracts.d.ts +37 -0
  11. package/dist/core/contracts/feature-pack-contracts.d.ts.map +1 -0
  12. package/dist/core/contracts/feature-pack-contracts.js +8 -0
  13. package/dist/core/contracts/feature-pack-contracts.js.map +1 -0
  14. package/dist/core/contracts/project-query-contracts.d.ts +84 -0
  15. package/dist/core/contracts/project-query-contracts.d.ts.map +1 -0
  16. package/dist/core/contracts/project-query-contracts.js +2 -0
  17. package/dist/core/contracts/project-query-contracts.js.map +1 -0
  18. package/dist/core/graph/code-graph-model.d.ts +9 -0
  19. package/dist/core/graph/code-graph-model.d.ts.map +1 -0
  20. package/dist/core/graph/code-graph-model.js +70 -0
  21. package/dist/core/graph/code-graph-model.js.map +1 -0
  22. package/dist/core/graph/code-graph-populator.d.ts +20 -0
  23. package/dist/core/graph/code-graph-populator.d.ts.map +1 -0
  24. package/dist/core/graph/code-graph-populator.js +760 -0
  25. package/dist/core/graph/code-graph-populator.js.map +1 -0
  26. package/dist/core/graph/contracts.d.ts +29 -0
  27. package/dist/core/graph/contracts.d.ts.map +1 -0
  28. package/dist/core/graph/contracts.js +47 -0
  29. package/dist/core/graph/contracts.js.map +1 -0
  30. package/dist/core/ingest/contracts.d.ts +44 -0
  31. package/dist/core/ingest/contracts.d.ts.map +1 -0
  32. package/dist/core/ingest/contracts.js +2 -0
  33. package/dist/core/ingest/contracts.js.map +1 -0
  34. package/dist/core/ingest/ids.d.ts +5 -0
  35. package/dist/core/ingest/ids.d.ts.map +1 -0
  36. package/dist/core/ingest/ids.js +17 -0
  37. package/dist/core/ingest/ids.js.map +1 -0
  38. package/dist/core/ingest/ingest-pipeline.d.ts +4 -0
  39. package/dist/core/ingest/ingest-pipeline.d.ts.map +1 -0
  40. package/dist/core/ingest/ingest-pipeline.js +105 -0
  41. package/dist/core/ingest/ingest-pipeline.js.map +1 -0
  42. package/dist/core/ingest/semantic-block-extractor.d.ts +9 -0
  43. package/dist/core/ingest/semantic-block-extractor.d.ts.map +1 -0
  44. package/dist/core/ingest/semantic-block-extractor.js +171 -0
  45. package/dist/core/ingest/semantic-block-extractor.js.map +1 -0
  46. package/dist/core/usecases/default-memory-usecase-port.d.ts +38 -0
  47. package/dist/core/usecases/default-memory-usecase-port.d.ts.map +1 -1
  48. package/dist/core/usecases/default-memory-usecase-port.js +1686 -12
  49. package/dist/core/usecases/default-memory-usecase-port.js.map +1 -1
  50. package/dist/db/graph-db.d.ts +24 -0
  51. package/dist/db/graph-db.d.ts.map +1 -1
  52. package/dist/db/graph-db.js +81 -2
  53. package/dist/db/graph-db.js.map +1 -1
  54. package/dist/db/slot-db.d.ts +235 -2
  55. package/dist/db/slot-db.d.ts.map +1 -1
  56. package/dist/db/slot-db.js +840 -18
  57. package/dist/db/slot-db.js.map +1 -1
  58. package/dist/index.d.ts +7 -247
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +32 -119
  61. package/dist/index.js.map +1 -1
  62. package/dist/shared/asm-config.d.ts +82 -0
  63. package/dist/shared/asm-config.d.ts.map +1 -0
  64. package/dist/shared/asm-config.js +254 -0
  65. package/dist/shared/asm-config.js.map +1 -0
  66. package/dist/shared/slotdb-path.d.ts +4 -3
  67. package/dist/shared/slotdb-path.d.ts.map +1 -1
  68. package/dist/shared/slotdb-path.js +15 -6
  69. package/dist/shared/slotdb-path.js.map +1 -1
  70. package/dist/tools/graph-tools.d.ts.map +1 -1
  71. package/dist/tools/graph-tools.js +131 -0
  72. package/dist/tools/graph-tools.js.map +1 -1
  73. package/dist/tools/project-tools.d.ts.map +1 -1
  74. package/dist/tools/project-tools.js +543 -0
  75. package/dist/tools/project-tools.js.map +1 -1
  76. package/openclaw.plugin.json +5 -164
  77. package/package.json +61 -26
  78. package/scripts/init-openclaw.mjs +727 -0
@@ -0,0 +1,727 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, copyFileSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { stdin as input, stdout as output } from "node:process";
6
+
7
+ const PLUGIN_ID = "agent-smart-memo";
8
+ const EMBED_BACKENDS = ["ollama", "openai", "docker"];
9
+ const DEFAULT_ASM_CONFIG_RELATIVE_PATH = ".config/asm/config.json";
10
+
11
+ export function resolveOpenClawConfigPath(env = process.env) {
12
+ const explicit = String(env.OPENCLAW_CONFIG_PATH || env.OPENCLAW_RUNTIME_CONFIG || "").trim();
13
+ if (explicit) return explicit;
14
+ const stateDir = String(env.OPENCLAW_STATE_DIR || "").trim() || `${env.HOME}/.openclaw`;
15
+ return join(stateDir, "openclaw.json");
16
+ }
17
+
18
+ function asObj(value) {
19
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
20
+ }
21
+
22
+ function firstNonEmptyString(...values) {
23
+ for (const value of values) {
24
+ if (typeof value === "string" && value.trim()) return value.trim();
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ function expandHome(input, homeDir) {
30
+ if (typeof input !== "string" || !input.startsWith("~")) return input;
31
+ const home = firstNonEmptyString(homeDir, process.env.HOME);
32
+ if (!home) return input;
33
+ if (input === "~") return home;
34
+ if (input.startsWith("~/")) return join(home, input.slice(2));
35
+ return input;
36
+ }
37
+
38
+ function resolveAsmConfigPathLocal({ env = process.env, homeDir, configPath } = {}) {
39
+ const explicit = firstNonEmptyString(configPath);
40
+ if (explicit) return explicit;
41
+ const envPath = firstNonEmptyString(env.ASM_CONFIG);
42
+ if (envPath) return envPath;
43
+ const home = firstNonEmptyString(homeDir, env.HOME, process.env.HOME);
44
+ if (!home) return DEFAULT_ASM_CONFIG_RELATIVE_PATH;
45
+ return join(home, DEFAULT_ASM_CONFIG_RELATIVE_PATH);
46
+ }
47
+
48
+ function parseJsonFile(path) {
49
+ if (!path || !existsSync(path)) return null;
50
+ const raw = readFileSync(path, "utf8").trim();
51
+ if (!raw) return null;
52
+ return JSON.parse(raw);
53
+ }
54
+
55
+ function loadAsmSharedConfigLocal({ env = process.env, homeDir, configPath } = {}) {
56
+ const path = resolveAsmConfigPathLocal({ env, homeDir, configPath });
57
+ try {
58
+ return { path, config: asObj(parseJsonFile(path)) };
59
+ } catch {
60
+ return { path, config: {} };
61
+ }
62
+ }
63
+
64
+ function toIntOrDefault(value, fallback) {
65
+ const n = Number(value);
66
+ return Number.isFinite(n) ? Math.trunc(n) : fallback;
67
+ }
68
+
69
+ export function parseExistingConfig(path) {
70
+ if (!existsSync(path)) return {};
71
+ const raw = readFileSync(path, "utf8").trim();
72
+ if (!raw) return {};
73
+ try {
74
+ return asObj(JSON.parse(raw));
75
+ } catch {
76
+ throw new Error(`Invalid JSON at ${path}`);
77
+ }
78
+ }
79
+
80
+ function dedupeStringArray(value) {
81
+ const list = Array.isArray(value) ? value : [];
82
+ const out = [];
83
+ const seen = new Set();
84
+ for (const item of list) {
85
+ const s = String(item || "").trim();
86
+ if (!s || seen.has(s)) continue;
87
+ seen.add(s);
88
+ out.push(s);
89
+ }
90
+ return out;
91
+ }
92
+
93
+ function yesNoNormalize(value, fallback = true) {
94
+ const txt = String(value || "").trim().toLowerCase();
95
+ if (!txt) return fallback;
96
+ if (["y", "yes", "1", "true"].includes(txt)) return true;
97
+ if (["n", "no", "0", "false"].includes(txt)) return false;
98
+ return fallback;
99
+ }
100
+
101
+ function normalizeTelegramCommandName(value) {
102
+ const normalized = String(value || "")
103
+ .trim()
104
+ .replace(/^\/+/, "")
105
+ .toLowerCase()
106
+ .replace(/-/g, "_")
107
+ .replace(/[^a-z0-9_]/g, "")
108
+ .slice(0, 32);
109
+
110
+ // ASM-84 follow-up: migrate legacy /addproject command naming to /project.
111
+ if (normalized === "addproject" || normalized === "add_project") {
112
+ return "project";
113
+ }
114
+
115
+ return normalized;
116
+ }
117
+
118
+ function isValidTelegramCommandName(value) {
119
+ return /^[a-z][a-z0-9_]{0,31}$/.test(String(value || ""));
120
+ }
121
+
122
+ function defaultTelegramCommandDescription(name) {
123
+ if (name === "project") return "Project onboarding";
124
+ if (name === "linkjira") return "Link Jira mapping";
125
+ if (name === "indexproject") return "Index registered project";
126
+ return `Run /${name}`;
127
+ }
128
+
129
+ function mergeTelegramCustomCommands(existing, commandNames) {
130
+ const current = Array.isArray(existing) ? existing : [];
131
+ const out = [];
132
+ const seen = new Set();
133
+
134
+ for (const item of current) {
135
+ const command = normalizeTelegramCommandName(item?.command);
136
+ const rawDescription = String(item?.description || "").trim();
137
+ const description =
138
+ command === "project" && /^add\s+project\s+onboarding$/i.test(rawDescription)
139
+ ? defaultTelegramCommandDescription(command)
140
+ : rawDescription;
141
+ if (!isValidTelegramCommandName(command) || seen.has(command)) continue;
142
+ seen.add(command);
143
+ out.push({ command, description: description || defaultTelegramCommandDescription(command) });
144
+ }
145
+
146
+ for (const rawName of commandNames || []) {
147
+ const command = normalizeTelegramCommandName(rawName);
148
+ if (!isValidTelegramCommandName(command) || seen.has(command)) continue;
149
+ seen.add(command);
150
+ out.push({ command, description: defaultTelegramCommandDescription(command) });
151
+ }
152
+
153
+ return out;
154
+ }
155
+
156
+ function asStringArray(value) {
157
+ if (!Array.isArray(value)) return [];
158
+ return value.map((item) => String(item || "").trim()).filter(Boolean);
159
+ }
160
+
161
+ function detectTelegramCustomCommandTargets(telegramConfig) {
162
+ const telegram = asObj(telegramConfig);
163
+ const accounts = asObj(telegram.accounts);
164
+ const accountKeys = Object.keys(accounts).filter((key) => key.trim().length > 0);
165
+
166
+ if (!accountKeys.length) {
167
+ return { mode: "single", accountKeys: [] };
168
+ }
169
+
170
+ const selected = new Set();
171
+ const scalarSelectors = [
172
+ telegram.account,
173
+ telegram.accountId,
174
+ telegram.activeAccount,
175
+ telegram.currentAccount,
176
+ telegram.defaultAccount,
177
+ telegram.selectedAccount,
178
+ ];
179
+
180
+ for (const raw of scalarSelectors) {
181
+ const key = String(raw || "").trim();
182
+ if (key && accountKeys.includes(key)) selected.add(key);
183
+ }
184
+
185
+ const arraySelectors = [
186
+ telegram.accountsEnabled,
187
+ telegram.enabledAccounts,
188
+ telegram.activeAccounts,
189
+ telegram.usedAccounts,
190
+ telegram.selectedAccounts,
191
+ ];
192
+
193
+ for (const arr of arraySelectors) {
194
+ for (const key of asStringArray(arr)) {
195
+ if (accountKeys.includes(key)) selected.add(key);
196
+ }
197
+ }
198
+
199
+ for (const key of accountKeys) {
200
+ const account = asObj(accounts[key]);
201
+ if (
202
+ account.enabled === true ||
203
+ account.isEnabled === true ||
204
+ account.active === true ||
205
+ account.inUse === true ||
206
+ account.selected === true ||
207
+ account.default === true
208
+ ) {
209
+ selected.add(key);
210
+ }
211
+ }
212
+
213
+ if (!selected.size) {
214
+ if (accountKeys.length === 1) {
215
+ selected.add(accountKeys[0]);
216
+ } else {
217
+ for (const key of accountKeys) {
218
+ const account = asObj(accounts[key]);
219
+ if (account.enabled !== false) selected.add(key);
220
+ }
221
+ }
222
+ }
223
+
224
+ return {
225
+ mode: "multi",
226
+ accountKeys: accountKeys.filter((key) => selected.has(key)),
227
+ };
228
+ }
229
+
230
+ function collectTelegramCommandNames(config, answers) {
231
+ const current = asObj(config);
232
+ const commands = [
233
+ ...(asObj(asObj(current.channels).telegram).customCommands || []),
234
+ ].map((item) => normalizeTelegramCommandName(item?.command));
235
+
236
+ const targets = detectTelegramCustomCommandTargets(asObj(asObj(current.channels).telegram));
237
+ if (targets.mode === "multi") {
238
+ const accounts = asObj(asObj(asObj(current.channels).telegram).accounts);
239
+ for (const accountKey of targets.accountKeys) {
240
+ commands.push(
241
+ ...((asObj(accounts[accountKey]).customCommands || [])
242
+ .map((item) => normalizeTelegramCommandName(item?.command))),
243
+ );
244
+ }
245
+ }
246
+
247
+ commands.push(...(Array.isArray(answers?.telegramOnboardingCommands) ? answers.telegramOnboardingCommands : []));
248
+ return dedupeStringArray(commands.filter((name) => isValidTelegramCommandName(name)));
249
+ }
250
+
251
+ export function validateAnswers(answers) {
252
+ const errors = [];
253
+
254
+ if (!String(answers.qdrantHost || "").trim()) errors.push("qdrantHost is required");
255
+ if (!Number.isInteger(answers.qdrantPort) || answers.qdrantPort <= 0) errors.push("qdrantPort must be a positive integer");
256
+ if (!String(answers.qdrantCollection || "").trim()) errors.push("qdrantCollection is required");
257
+
258
+ if (!String(answers.llmBaseUrl || "").trim()) errors.push("llmBaseUrl is required");
259
+ if (!String(answers.llmModel || "").trim()) errors.push("llmModel is required");
260
+
261
+ if (!String(answers.embedBackend || "").trim()) {
262
+ errors.push("embedBackend is required");
263
+ } else if (!EMBED_BACKENDS.includes(String(answers.embedBackend))) {
264
+ errors.push(`embedBackend must be one of: ${EMBED_BACKENDS.join(", ")}`);
265
+ }
266
+
267
+ if (!String(answers.embedModel || "").trim()) errors.push("embedModel is required");
268
+ if (!Number.isInteger(answers.embedDimensions) || answers.embedDimensions <= 0) {
269
+ errors.push("embedDimensions must be a positive integer");
270
+ }
271
+
272
+ if (!String(answers.slotDbDir || "").trim()) errors.push("slotDbDir is required");
273
+
274
+ const projectWorkspaceRoot = String(
275
+ answers.projectWorkspaceRoot || "",
276
+ ).trim();
277
+ if (!projectWorkspaceRoot) {
278
+ errors.push("projectWorkspaceRoot is required");
279
+ }
280
+
281
+ const onboardingCommands = Array.isArray(answers.telegramOnboardingCommands)
282
+ ? answers.telegramOnboardingCommands
283
+ : [];
284
+ for (const name of onboardingCommands) {
285
+ const normalized = normalizeTelegramCommandName(name);
286
+ if (!isValidTelegramCommandName(normalized)) {
287
+ errors.push(`invalid telegram command name: ${String(name)}`);
288
+ }
289
+ }
290
+
291
+ return errors;
292
+ }
293
+
294
+ export function buildPatchedConfig(existingConfig, answers, mapMemorySlot = true, options = {}) {
295
+ const root = asObj(existingConfig);
296
+ const plugins = asObj(root.plugins);
297
+ const entries = asObj(plugins.entries);
298
+ const slots = asObj(plugins.slots);
299
+
300
+ const allow = dedupeStringArray(plugins.allow);
301
+ if (!allow.includes(PLUGIN_ID)) allow.push(PLUGIN_ID);
302
+
303
+ const prevEntry = asObj(entries[PLUGIN_ID]);
304
+ const prevEntryConfig = asObj(prevEntry.config);
305
+ const asmConfigPath = String(options.asmConfigPath || prevEntryConfig.asmConfigPath || "").trim();
306
+
307
+ const entry = {
308
+ ...prevEntry,
309
+ enabled: true,
310
+ config: {
311
+ ...(asmConfigPath ? { asmConfigPath } : {}),
312
+ },
313
+ };
314
+
315
+ const nextSlots = { ...slots };
316
+ if (mapMemorySlot) {
317
+ nextSlots.memory = PLUGIN_ID;
318
+ } else if (nextSlots.memory === PLUGIN_ID) {
319
+ delete nextSlots.memory;
320
+ }
321
+
322
+ const channels = asObj(root.channels);
323
+ const telegram = asObj(channels.telegram);
324
+ const targets = detectTelegramCustomCommandTargets(telegram);
325
+
326
+ let nextTelegram;
327
+ if (targets.mode === "single") {
328
+ nextTelegram = {
329
+ ...telegram,
330
+ customCommands: mergeTelegramCustomCommands(
331
+ telegram.customCommands,
332
+ answers.telegramOnboardingCommands || [],
333
+ ),
334
+ };
335
+ } else {
336
+ const prevAccounts = asObj(telegram.accounts);
337
+ const nextAccounts = { ...prevAccounts };
338
+
339
+ for (const accountKey of targets.accountKeys) {
340
+ const account = asObj(prevAccounts[accountKey]);
341
+ nextAccounts[accountKey] = {
342
+ ...account,
343
+ customCommands: mergeTelegramCustomCommands(
344
+ account.customCommands,
345
+ answers.telegramOnboardingCommands || [],
346
+ ),
347
+ };
348
+ }
349
+
350
+ nextTelegram = {
351
+ ...telegram,
352
+ accounts: nextAccounts,
353
+ };
354
+ }
355
+
356
+ return {
357
+ ...root,
358
+ channels: {
359
+ ...channels,
360
+ telegram: nextTelegram,
361
+ },
362
+ plugins: {
363
+ ...plugins,
364
+ allow,
365
+ slots: nextSlots,
366
+ entries: {
367
+ ...entries,
368
+ [PLUGIN_ID]: entry,
369
+ },
370
+ },
371
+ };
372
+ }
373
+
374
+ export function buildBackupPath(configPath, now = new Date()) {
375
+ const pad = (n) => String(n).padStart(2, "0");
376
+ const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
377
+ return `${configPath}.bak.${ts}`;
378
+ }
379
+
380
+ function toJson(value) {
381
+ return `${JSON.stringify(value, null, 2)}\n`;
382
+ }
383
+
384
+ function isSameValue(a, b) {
385
+ return JSON.stringify(a) === JSON.stringify(b);
386
+ }
387
+
388
+ function classifySummaryItem(summary, label, beforeValue, afterValue) {
389
+ if (isSameValue(beforeValue, afterValue)) {
390
+ summary.alreadyConfigured.push(label);
391
+ return;
392
+ }
393
+
394
+ if (beforeValue === false && afterValue === true) {
395
+ summary.willAdd.push(label);
396
+ return;
397
+ }
398
+
399
+ if (typeof beforeValue === "undefined" && typeof afterValue !== "undefined") {
400
+ summary.willAdd.push(label);
401
+ return;
402
+ }
403
+
404
+ summary.willUpdate.push(label);
405
+ }
406
+
407
+ export function buildSetupSummary(currentConfig, answers, nextConfig) {
408
+ const current = asObj(currentConfig);
409
+ const next = asObj(nextConfig || buildPatchedConfig(current, answers, answers.mapMemorySlot));
410
+
411
+ const currentPlugins = asObj(current.plugins);
412
+ const nextPlugins = asObj(next.plugins);
413
+ const currentEntries = asObj(currentPlugins.entries);
414
+ const nextEntries = asObj(nextPlugins.entries);
415
+ const currentEntry = asObj(currentEntries[PLUGIN_ID]);
416
+ const nextEntry = asObj(nextEntries[PLUGIN_ID]);
417
+ const currentEntryConfig = asObj(currentEntry.config);
418
+ const nextEntryConfig = asObj(nextEntry.config);
419
+
420
+ const summary = {
421
+ alreadyConfigured: [],
422
+ willAdd: [],
423
+ willUpdate: [],
424
+ };
425
+
426
+ const currentAllowHasPlugin = dedupeStringArray(currentPlugins.allow).includes(PLUGIN_ID);
427
+ const nextAllowHasPlugin = dedupeStringArray(nextPlugins.allow).includes(PLUGIN_ID);
428
+ classifySummaryItem(summary, `plugins.allow includes ${PLUGIN_ID}`, currentAllowHasPlugin, nextAllowHasPlugin);
429
+
430
+ classifySummaryItem(
431
+ summary,
432
+ `plugins.entries.${PLUGIN_ID} exists`,
433
+ Object.keys(currentEntry).length > 0,
434
+ Object.keys(nextEntry).length > 0,
435
+ );
436
+
437
+ const managedConfigKeys = [
438
+ "asmConfigPath",
439
+ ];
440
+
441
+ for (const key of managedConfigKeys) {
442
+ classifySummaryItem(
443
+ summary,
444
+ `plugins.entries.${PLUGIN_ID}.config.${key}`,
445
+ currentEntryConfig[key],
446
+ nextEntryConfig[key],
447
+ );
448
+ }
449
+
450
+ classifySummaryItem(
451
+ summary,
452
+ "plugins.slots.memory",
453
+ asObj(currentPlugins.slots).memory,
454
+ asObj(nextPlugins.slots).memory,
455
+ );
456
+
457
+ const currentTelegram = asObj(asObj(current.channels).telegram);
458
+ const nextTelegram = asObj(asObj(next.channels).telegram);
459
+ const currentTargets = detectTelegramCustomCommandTargets(currentTelegram);
460
+ const nextTargets = detectTelegramCustomCommandTargets(nextTelegram);
461
+ const commandNames = collectTelegramCommandNames(next, answers);
462
+
463
+ if (nextTargets.mode === "single") {
464
+ const currentTelegramCommands = dedupeStringArray(
465
+ (currentTelegram.customCommands || [])
466
+ .map((item) => normalizeTelegramCommandName(item?.command))
467
+ .filter((name) => isValidTelegramCommandName(name)),
468
+ );
469
+
470
+ const nextTelegramCommands = dedupeStringArray(
471
+ (nextTelegram.customCommands || [])
472
+ .map((item) => normalizeTelegramCommandName(item?.command))
473
+ .filter((name) => isValidTelegramCommandName(name)),
474
+ );
475
+
476
+ for (const commandName of commandNames) {
477
+ classifySummaryItem(
478
+ summary,
479
+ `channels.telegram.customCommands includes /${commandName}`,
480
+ currentTelegramCommands.includes(commandName),
481
+ nextTelegramCommands.includes(commandName),
482
+ );
483
+ }
484
+ } else {
485
+ const accountKeys = dedupeStringArray(nextTargets.accountKeys);
486
+ const currentAccounts = asObj(currentTelegram.accounts);
487
+ const nextAccounts = asObj(nextTelegram.accounts);
488
+
489
+ for (const accountKey of accountKeys) {
490
+ const currentTelegramCommands = dedupeStringArray(
491
+ (asObj(currentAccounts[accountKey]).customCommands || [])
492
+ .map((item) => normalizeTelegramCommandName(item?.command))
493
+ .filter((name) => isValidTelegramCommandName(name)),
494
+ );
495
+
496
+ const nextTelegramCommands = dedupeStringArray(
497
+ (asObj(nextAccounts[accountKey]).customCommands || [])
498
+ .map((item) => normalizeTelegramCommandName(item?.command))
499
+ .filter((name) => isValidTelegramCommandName(name)),
500
+ );
501
+
502
+ for (const commandName of commandNames) {
503
+ classifySummaryItem(
504
+ summary,
505
+ `channels.telegram.accounts.${accountKey}.customCommands includes /${commandName}`,
506
+ currentTelegramCommands.includes(commandName),
507
+ nextTelegramCommands.includes(commandName),
508
+ );
509
+ }
510
+ }
511
+
512
+ if (currentTargets.mode !== "multi" && accountKeys.length > 0) {
513
+ summary.willUpdate.push("channels.telegram.customCommands scope switched to account fan-out");
514
+ }
515
+ }
516
+
517
+ return summary;
518
+ }
519
+
520
+ export function formatSetupSummary(summary) {
521
+ const sections = [
522
+ ["already configured", summary.alreadyConfigured],
523
+ ["will add", summary.willAdd],
524
+ ["will update", summary.willUpdate],
525
+ ];
526
+
527
+ const lines = ["[ASM-83] Setup summary (before confirm):"];
528
+ for (const [label, items] of sections) {
529
+ lines.push(`- ${label} (${items.length})`);
530
+ if (!items.length) {
531
+ lines.push(" • (none)");
532
+ continue;
533
+ }
534
+
535
+ for (const item of items) {
536
+ lines.push(` • ${item}`);
537
+ }
538
+ }
539
+
540
+ return lines.join("\n");
541
+ }
542
+
543
+ function previewDiff(beforeText, afterText) {
544
+ if (beforeText === afterText) return "(no change)";
545
+ const before = beforeText.split("\n");
546
+ const after = afterText.split("\n");
547
+ const max = Math.max(before.length, after.length);
548
+ const lines = [];
549
+
550
+ for (let i = 0; i < max; i += 1) {
551
+ const b = before[i];
552
+ const a = after[i];
553
+ if (b === a) continue;
554
+ if (typeof b === "string") lines.push(`- ${b}`);
555
+ if (typeof a === "string") lines.push(`+ ${a}`);
556
+ if (lines.length >= 160) {
557
+ lines.push("... diff truncated ...");
558
+ break;
559
+ }
560
+ }
561
+
562
+ return lines.join("\n");
563
+ }
564
+
565
+ async function promptWizard(defaults) {
566
+ const rl = createInterface({ input, output });
567
+ const ask = async (label, current, secret = false) => {
568
+ if (secret) {
569
+ const answer = await rl.question(`${label} [hidden, press Enter to keep current]: `);
570
+ if (!String(answer || "").trim()) return current;
571
+ return String(answer).trim();
572
+ }
573
+
574
+ const answer = await rl.question(`${label} [${current ?? ""}]: `);
575
+ return String(answer || "").trim() || current;
576
+ };
577
+
578
+ try {
579
+ const qdrantHost = await ask("Qdrant host", defaults.qdrantHost);
580
+ const qdrantPort = toIntOrDefault(await ask("Qdrant port", String(defaults.qdrantPort)), defaults.qdrantPort);
581
+ const qdrantCollection = await ask("Qdrant collection", defaults.qdrantCollection);
582
+
583
+ const llmBaseUrl = await ask("LLM base URL", defaults.llmBaseUrl);
584
+ const llmModel = await ask("LLM model", defaults.llmModel);
585
+ const llmApiKey = await ask("LLM API key", defaults.llmApiKey, true);
586
+
587
+ const embedBackend = await ask(`Embedding backend (${EMBED_BACKENDS.join("/")})`, defaults.embedBackend);
588
+ const embedModel = await ask("Embedding model", defaults.embedModel);
589
+ const embedDimensions = toIntOrDefault(await ask("Embedding dimensions", String(defaults.embedDimensions)), defaults.embedDimensions);
590
+
591
+ const slotDbDir = await ask("slotDbDir", defaults.slotDbDir);
592
+ const projectWorkspaceRoot = await ask("projectWorkspaceRoot", defaults.projectWorkspaceRoot);
593
+
594
+ const mapMemorySlotRaw = await ask("Map plugins.slots.memory = agent-smart-memo? (y/n)", defaults.mapMemorySlot ? "y" : "n");
595
+ const mapMemorySlot = yesNoNormalize(mapMemorySlotRaw, defaults.mapMemorySlot);
596
+
597
+ const onboardingCommandsRaw = await ask(
598
+ "Telegram custom onboarding commands (comma-separated)",
599
+ (defaults.telegramOnboardingCommands || []).join(","),
600
+ );
601
+ const telegramOnboardingCommands = dedupeStringArray(
602
+ String(onboardingCommandsRaw || "")
603
+ .split(",")
604
+ .map((item) => normalizeTelegramCommandName(item)),
605
+ ).filter((name) => isValidTelegramCommandName(name));
606
+
607
+ return {
608
+ qdrantHost,
609
+ qdrantPort,
610
+ qdrantCollection,
611
+ llmBaseUrl,
612
+ llmModel,
613
+ llmApiKey,
614
+ embedBackend,
615
+ embedModel,
616
+ embedDimensions,
617
+ slotDbDir,
618
+ projectWorkspaceRoot,
619
+ mapMemorySlot,
620
+ telegramOnboardingCommands,
621
+ };
622
+ } finally {
623
+ rl.close();
624
+ }
625
+ }
626
+
627
+ export async function runInitOpenClaw({ env = process.env, interactive = true, autoApply = false } = {}) {
628
+ const configPath = resolveOpenClawConfigPath(env);
629
+ const current = parseExistingConfig(configPath);
630
+ const pluginCfg = asObj(asObj(asObj(current.plugins).entries)[PLUGIN_ID]).config || {};
631
+ const asmConfigPath = String(pluginCfg.asmConfigPath || resolveAsmConfigPathLocal({ env, homeDir: env.HOME })).trim();
632
+ const shared = loadAsmSharedConfigLocal({ configPath: asmConfigPath, env, homeDir: env.HOME }).config || {};
633
+ const sharedCore = asObj(shared.core);
634
+
635
+ const defaults = {
636
+ qdrantHost: String(sharedCore.qdrantHost || "localhost"),
637
+ qdrantPort: toIntOrDefault(sharedCore.qdrantPort, 6333),
638
+ qdrantCollection: String(sharedCore.qdrantCollection || "mrc_bot"),
639
+ llmBaseUrl: String(sharedCore.llmBaseUrl || "http://localhost:8317/v1"),
640
+ llmModel: String(sharedCore.llmModel || "gemini-2.5-flash"),
641
+ llmApiKey: String(sharedCore.llmApiKey || ""),
642
+ embedBackend: String(sharedCore.embedBackend || "ollama"),
643
+ embedModel: String(sharedCore.embedModel || "qwen3-embedding:0.6b"),
644
+ embedDimensions: toIntOrDefault(sharedCore.embedDimensions, 1024),
645
+ slotDbDir: String(sharedCore.storage?.slotDbDir || env.OPENCLAW_SLOTDB_DIR || `${env.HOME}/.openclaw/agent-memo`),
646
+ projectWorkspaceRoot: String(
647
+ sharedCore.projectWorkspaceRoot ||
648
+ env.AGENT_MEMO_PROJECT_WORKSPACE_ROOT ||
649
+ env.AGENT_MEMO_REPO_CLONE_ROOT ||
650
+ `${env.HOME}/Work/projects` ||
651
+ `${env.HOME}/.openclaw/workspace/projects`,
652
+ ),
653
+ asmConfigPath,
654
+ mapMemorySlot: asObj(asObj(current.plugins).slots).memory === PLUGIN_ID,
655
+ telegramOnboardingCommands: dedupeStringArray([
656
+ "project",
657
+ ...collectTelegramCommandNames(current, { telegramOnboardingCommands: [] }),
658
+ ]),
659
+ };
660
+
661
+ const answers = interactive ? await promptWizard(defaults) : defaults;
662
+ const errors = validateAnswers(answers);
663
+ if (errors.length > 0) {
664
+ throw new Error(`Validation failed:\n- ${errors.join("\n- ")}`);
665
+ }
666
+
667
+ const next = buildPatchedConfig(current, answers, answers.mapMemorySlot, { asmConfigPath: answers.asmConfigPath });
668
+ const summary = buildSetupSummary(current, answers, next);
669
+ const beforeText = toJson(current);
670
+ const afterText = toJson(next);
671
+
672
+ console.log(`\n[ASM-83] Config path: ${configPath}`);
673
+ if (!existsSync(configPath)) {
674
+ console.log("[ASM-83] openclaw.json not found. A new file will be created.");
675
+ }
676
+
677
+ console.log(`\n${formatSetupSummary(summary)}\n`);
678
+
679
+ console.log("[ASM-83] Preview diff:\n");
680
+ console.log(previewDiff(beforeText, afterText));
681
+
682
+ if (!autoApply) {
683
+ const rl = createInterface({ input, output });
684
+ try {
685
+ const confirm = await rl.question("\nApply changes and write config? (y/N): ");
686
+ if (!yesNoNormalize(confirm, false)) {
687
+ console.log("[ASM-83] Aborted by user. No file written.");
688
+ return { applied: false, configPath };
689
+ }
690
+ } finally {
691
+ rl.close();
692
+ }
693
+ } else {
694
+ console.log("\n[ASM-83] Auto-apply mode enabled. Writing config without confirmation prompt.");
695
+ }
696
+
697
+ mkdirSync(dirname(configPath), { recursive: true });
698
+
699
+ let backupPath = null;
700
+ if (existsSync(configPath)) {
701
+ backupPath = buildBackupPath(configPath, new Date());
702
+ copyFileSync(configPath, backupPath);
703
+ console.log(`[ASM-83] Backup created: ${backupPath}`);
704
+ }
705
+
706
+ writeFileSync(configPath, afterText, "utf8");
707
+ console.log(`[ASM-83] Config updated: ${configPath}`);
708
+
709
+ console.log("\n[ASM-83] Next steps:");
710
+ console.log("1) Restart OpenClaw runtime (if running)");
711
+ console.log("2) Verify plugin is loaded: agent-smart-memo");
712
+ console.log("3) Optional: run a memory tool smoke test (memory_slot_set/get)");
713
+
714
+ return {
715
+ applied: true,
716
+ configPath,
717
+ backupPath,
718
+ };
719
+ }
720
+
721
+ if (import.meta.url === `file://${process.argv[1]}`) {
722
+ runInitOpenClaw({ interactive: true })
723
+ .catch((error) => {
724
+ console.error(`[ASM-83] Failed: ${error instanceof Error ? error.message : String(error)}`);
725
+ process.exitCode = 1;
726
+ });
727
+ }