@heyamiko/amiko-cli 0.9.8 → 0.9.9-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.
Files changed (2) hide show
  1. package/dist/index.js +342 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -22012,6 +22012,347 @@ function registerNotificationsCommand(program2) {
22012
22012
  });
22013
22013
  }
22014
22014
 
22015
+ // src/commands/memory.ts
22016
+ import { existsSync as existsSync3, readFileSync as readFileSync2, statSync } from "node:fs";
22017
+ import { dirname, isAbsolute, normalize, resolve as resolve2 } from "node:path";
22018
+ var VALID_CATEGORIES = ["fact", "preference", "pattern", "decision", "context"];
22019
+ var VALID_SOURCES = ["chat", "file", "manual"];
22020
+ var DEFAULT_WORKSPACE = "/data/.openclaw/workspace";
22021
+ var SYNC_BATCH_SIZE = 100;
22022
+ function fail(msg) {
22023
+ console.error(error(msg));
22024
+ process.exit(1);
22025
+ }
22026
+ function parseTags(raw) {
22027
+ if (!raw)
22028
+ return;
22029
+ const tags = raw.split(",").map((t) => t.trim()).filter(Boolean);
22030
+ return tags.length ? tags : undefined;
22031
+ }
22032
+ function fmtMemory(m) {
22033
+ console.log("");
22034
+ console.log(label("ID", m.id));
22035
+ console.log(label("Category", m.category));
22036
+ console.log(label("Source", m.source));
22037
+ if (m.tags && m.tags.length)
22038
+ console.log(label("Tags", m.tags.join(", ")));
22039
+ if (typeof m.score === "number")
22040
+ console.log(label("Score", m.score.toFixed(3)));
22041
+ if (m.created_at)
22042
+ console.log(label("Created", m.created_at.slice(0, 19)));
22043
+ console.log(m.content);
22044
+ }
22045
+ function registerMemoryCommand(program2) {
22046
+ const memory = program2.command("memory").description("Manage cross-agent memories — add, list, search, remove, sync local files");
22047
+ memory.command("add <content>").description("Add a single memory directly").option("--category <cat>", `Category (${VALID_CATEGORIES.join("|")})`, "fact").option("--tags <list>", "Comma-separated tags (max 10)").option("--source <src>", `Source (${VALID_SOURCES.join("|")})`, "manual").option("--twin <twinId>", "Override twin_id (defaults to scopedTwinId)").option("--json", "Output as JSON").action(async (content, opts) => {
22048
+ if (!content || !content.trim())
22049
+ fail("Content is required and cannot be empty.");
22050
+ if (opts.category && !VALID_CATEGORIES.includes(opts.category)) {
22051
+ fail(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`);
22052
+ }
22053
+ if (opts.source && !VALID_SOURCES.includes(opts.source)) {
22054
+ fail(`Invalid source. Must be one of: ${VALID_SOURCES.join(", ")}`);
22055
+ }
22056
+ const tags = parseTags(opts.tags);
22057
+ if (tags && tags.length > 10)
22058
+ fail("Max 10 tags.");
22059
+ const auth = resolveAuth();
22060
+ const spinner = opts.json ? null : ora("Saving memory...").start();
22061
+ try {
22062
+ const data = await amikoWebFetch(auth, "/api/memories", {
22063
+ method: "POST",
22064
+ body: {
22065
+ content: content.trim(),
22066
+ category: opts.category,
22067
+ tags,
22068
+ source: opts.source,
22069
+ twin_id: opts.twin
22070
+ }
22071
+ });
22072
+ spinner?.stop();
22073
+ renderOutput(data, (d) => {
22074
+ console.log(success("Memory saved"));
22075
+ fmtMemory(d.memory);
22076
+ }, { json: opts.json });
22077
+ } catch (e) {
22078
+ spinner?.stop();
22079
+ fail(e instanceof Error ? e.message : String(e));
22080
+ }
22081
+ });
22082
+ memory.command("list").description("List the caller's memories (newest first)").option("--limit <n>", "Max results (1–100)", "25").option("--offset <n>", "Pagination offset", "0").option("--category <cat>", `Filter (${VALID_CATEGORIES.join("|")})`).option("--source <src>", `Filter (${VALID_SOURCES.join("|")})`).option("--json", "Output as JSON").action(async (opts) => {
22083
+ if (opts.category && !VALID_CATEGORIES.includes(opts.category)) {
22084
+ fail(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`);
22085
+ }
22086
+ if (opts.source && !VALID_SOURCES.includes(opts.source)) {
22087
+ fail(`Invalid source. Must be one of: ${VALID_SOURCES.join(", ")}`);
22088
+ }
22089
+ const auth = resolveAuth();
22090
+ const spinner = opts.json ? null : ora("Loading memories...").start();
22091
+ try {
22092
+ const data = await amikoWebFetch(auth, "/api/memories", {
22093
+ query: {
22094
+ limit: opts.limit,
22095
+ offset: opts.offset,
22096
+ category: opts.category,
22097
+ source: opts.source
22098
+ }
22099
+ });
22100
+ spinner?.stop();
22101
+ renderOutput(data, (d) => {
22102
+ if (!d.items.length) {
22103
+ console.log(dim("No memories."));
22104
+ return;
22105
+ }
22106
+ console.log(heading(`Memories — ${d.items.length} of ${d.total}`));
22107
+ for (const m of d.items)
22108
+ fmtMemory(m);
22109
+ if (d.has_more) {
22110
+ const next = Number(opts.offset || "0") + d.items.length;
22111
+ console.log("");
22112
+ console.log(dim(`More available — pass --offset ${next}`));
22113
+ }
22114
+ }, { json: opts.json });
22115
+ } catch (e) {
22116
+ spinner?.stop();
22117
+ fail(e instanceof Error ? e.message : String(e));
22118
+ }
22119
+ });
22120
+ memory.command("search <query>").description("Hybrid search across the caller's memories").option("--limit <n>", "Max results (1–100)", "25").option("--category <cat>", `Filter (${VALID_CATEGORIES.join("|")})`).option("--json", "Output as JSON").action(async (query, opts) => {
22121
+ if (!query || !query.trim())
22122
+ fail("Query is required.");
22123
+ if (opts.category && !VALID_CATEGORIES.includes(opts.category)) {
22124
+ fail(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`);
22125
+ }
22126
+ const auth = resolveAuth();
22127
+ const spinner = opts.json ? null : ora("Searching...").start();
22128
+ try {
22129
+ const data = await amikoWebFetch(auth, "/api/memories", {
22130
+ query: {
22131
+ q: query,
22132
+ limit: opts.limit,
22133
+ category: opts.category
22134
+ }
22135
+ });
22136
+ spinner?.stop();
22137
+ renderOutput(data, (d) => {
22138
+ if (!d.items.length) {
22139
+ console.log(dim("No matches."));
22140
+ return;
22141
+ }
22142
+ console.log(heading(`Matches — ${d.items.length} of ${d.total}`));
22143
+ for (const m of d.items)
22144
+ fmtMemory(m);
22145
+ }, { json: opts.json });
22146
+ } catch (e) {
22147
+ spinner?.stop();
22148
+ fail(e instanceof Error ? e.message : String(e));
22149
+ }
22150
+ });
22151
+ memory.command("rm <id>").description("Soft-delete a memory by id").option("--json", "Output as JSON").action(async (id, opts) => {
22152
+ if (!id)
22153
+ fail("Memory id is required.");
22154
+ const auth = resolveAuth();
22155
+ const spinner = opts.json ? null : ora("Deleting...").start();
22156
+ try {
22157
+ const data = await amikoWebFetch(auth, `/api/memories/${encodeURIComponent(id)}`, { method: "DELETE" });
22158
+ spinner?.stop();
22159
+ renderOutput(data, () => console.log(success(`Deleted memory ${id}`)), { json: opts.json });
22160
+ } catch (e) {
22161
+ spinner?.stop();
22162
+ fail(e instanceof Error ? e.message : String(e));
22163
+ }
22164
+ });
22165
+ memory.command("status").description("Show counts of memories and synced memory files").option("--json", "Output as JSON").action(async (opts) => {
22166
+ const auth = resolveAuth();
22167
+ const spinner = opts.json ? null : ora("Loading status...").start();
22168
+ try {
22169
+ const [memories, files] = await Promise.all([
22170
+ amikoWebFetch(auth, "/api/memories", {
22171
+ query: { limit: "1" }
22172
+ }),
22173
+ amikoWebFetch(auth, "/api/memory-files", { query: { limit: "1" } })
22174
+ ]);
22175
+ spinner?.stop();
22176
+ const fileTotal = files.total ?? files.items?.length ?? 0;
22177
+ renderOutput({ memories: memories.total, memory_files: fileTotal }, (d) => {
22178
+ console.log(heading("Memory status"));
22179
+ console.log(label("Memories", String(d.memories)));
22180
+ console.log(label("Memory files", String(d.memory_files)));
22181
+ }, { json: opts.json });
22182
+ } catch (e) {
22183
+ spinner?.stop();
22184
+ fail(e instanceof Error ? e.message : String(e));
22185
+ }
22186
+ });
22187
+ memory.command("sync").description("Sync MEMORY.md and referenced files to the platform (one-way: local → server)").option("--path <dir>", `Workspace dir containing MEMORY.md (default ${DEFAULT_WORKSPACE})`).option("--dry-run", "Print what would be synced without uploading").option("--prune", "Delete server-side memory files that no longer exist locally").option("--json", "Output as JSON").action(async (opts) => {
22188
+ const root = resolve2(opts.path || DEFAULT_WORKSPACE);
22189
+ const indexPath = resolve2(root, "MEMORY.md");
22190
+ if (!existsSync3(indexPath)) {
22191
+ fail(`MEMORY.md not found at ${indexPath}`);
22192
+ }
22193
+ const collected = collectMemoryFiles(root, indexPath);
22194
+ if (collected.errors.length && !opts.json) {
22195
+ for (const e of collected.errors)
22196
+ console.error(error(e));
22197
+ }
22198
+ const files = collected.files;
22199
+ if (opts.dryRun) {
22200
+ renderOutput({ root, files: files.map((f) => f.path), prune: !!opts.prune }, (d) => {
22201
+ console.log(heading(`Dry run — ${d.files.length} file(s)`));
22202
+ for (const p of d.files)
22203
+ console.log(` ${p}`);
22204
+ if (d.prune)
22205
+ console.log(dim("--prune would remove server files not listed above."));
22206
+ }, { json: opts.json });
22207
+ return;
22208
+ }
22209
+ if (!files.length) {
22210
+ renderOutput({ synced: 0, changed: 0, errors: 0, extracted: 0 }, () => console.log(dim("Nothing to sync.")), { json: opts.json });
22211
+ return;
22212
+ }
22213
+ const auth = resolveAuth();
22214
+ const spinner = opts.json ? null : ora(`Syncing ${files.length} file(s)...`).start();
22215
+ try {
22216
+ const allResults = [];
22217
+ for (let i = 0;i < files.length; i += SYNC_BATCH_SIZE) {
22218
+ const chunk = files.slice(i, i + SYNC_BATCH_SIZE);
22219
+ const data = await amikoWebFetch(auth, "/api/memory-files/bulk", {
22220
+ method: "POST",
22221
+ body: { files: chunk }
22222
+ });
22223
+ allResults.push(...data.results);
22224
+ }
22225
+ let pruned = 0;
22226
+ const pruneErrors = [];
22227
+ if (opts.prune) {
22228
+ const localPaths = new Set(files.map((f) => f.path));
22229
+ const remote = await fetchAllMemoryFiles(auth);
22230
+ const stale = remote.filter((f) => !localPaths.has(f.path));
22231
+ for (const f of stale) {
22232
+ try {
22233
+ await amikoWebFetch(auth, `/api/memory-files/${encodeURIComponent(f.id)}`, { method: "DELETE" });
22234
+ pruned++;
22235
+ } catch (e) {
22236
+ pruneErrors.push(`${f.path}: ${e instanceof Error ? e.message : String(e)}`);
22237
+ }
22238
+ }
22239
+ }
22240
+ spinner?.stop();
22241
+ const changed = allResults.filter((r) => r.changed).length;
22242
+ const errors2 = allResults.filter((r) => r.error).length;
22243
+ const extracted = allResults.reduce((acc, r) => acc + (r.extracted ?? 0), 0);
22244
+ renderOutput({
22245
+ synced: allResults.length,
22246
+ changed,
22247
+ errors: errors2,
22248
+ extracted,
22249
+ pruned,
22250
+ prune_errors: pruneErrors,
22251
+ results: allResults
22252
+ }, (d) => {
22253
+ console.log(success(`${d.synced} file(s) synced (${d.changed} changed, ${d.errors} errors), ${d.extracted} memories extracted`));
22254
+ for (const r of allResults) {
22255
+ if (r.error) {
22256
+ console.log(error(`${r.path}: ${r.error}`));
22257
+ } else if (r.changed) {
22258
+ console.log(` ${r.path} ${dim(`→ ${r.extracted ?? 0} extracted`)}`);
22259
+ } else {
22260
+ console.log(` ${r.path} ${dim("(unchanged)")}`);
22261
+ }
22262
+ }
22263
+ if (opts.prune) {
22264
+ console.log("");
22265
+ console.log(label("Pruned", String(d.pruned)));
22266
+ for (const e of pruneErrors)
22267
+ console.log(error(e));
22268
+ }
22269
+ }, { json: opts.json });
22270
+ } catch (e) {
22271
+ spinner?.stop();
22272
+ fail(e instanceof Error ? e.message : String(e));
22273
+ }
22274
+ });
22275
+ }
22276
+ function collectMemoryFiles(root, indexPath) {
22277
+ const errors2 = [];
22278
+ const seen = new Set;
22279
+ const files = [];
22280
+ const indexContent = safeRead(indexPath, errors2);
22281
+ if (indexContent === null)
22282
+ return { files, errors: errors2 };
22283
+ pushFile(files, seen, indexPath, indexContent, root);
22284
+ const indexDir = dirname(indexPath);
22285
+ for (const ref of extractMarkdownLinks(indexContent)) {
22286
+ if (isExternal(ref))
22287
+ continue;
22288
+ const abs = normalize(isAbsolute(ref) ? ref : resolve2(indexDir, ref));
22289
+ if (!abs.startsWith(root)) {
22290
+ errors2.push(`Skipping ${ref}: outside workspace ${root}`);
22291
+ continue;
22292
+ }
22293
+ if (!existsSync3(abs)) {
22294
+ errors2.push(`Referenced file missing: ${ref}`);
22295
+ continue;
22296
+ }
22297
+ const st = statSync(abs);
22298
+ if (!st.isFile())
22299
+ continue;
22300
+ const content = safeRead(abs, errors2);
22301
+ if (content === null)
22302
+ continue;
22303
+ pushFile(files, seen, abs, content, root);
22304
+ }
22305
+ return { files, errors: errors2 };
22306
+ }
22307
+ function pushFile(files, seen, abs, content, root) {
22308
+ const key = relativeKey(abs, root);
22309
+ if (seen.has(key))
22310
+ return;
22311
+ seen.add(key);
22312
+ files.push({ path: key, content });
22313
+ }
22314
+ function relativeKey(abs, root) {
22315
+ const rel = abs.startsWith(root) ? abs.slice(root.length) : abs;
22316
+ return rel.replace(/^\/+/, "");
22317
+ }
22318
+ function safeRead(path2, errors2) {
22319
+ try {
22320
+ return readFileSync2(path2, "utf-8");
22321
+ } catch (e) {
22322
+ errors2.push(`Failed to read ${path2}: ${e instanceof Error ? e.message : String(e)}`);
22323
+ return null;
22324
+ }
22325
+ }
22326
+ function extractMarkdownLinks(md) {
22327
+ const out = [];
22328
+ const re = /\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
22329
+ let m;
22330
+ while ((m = re.exec(md)) !== null) {
22331
+ const target = m[1].split("#")[0];
22332
+ if (target)
22333
+ out.push(target);
22334
+ }
22335
+ return out;
22336
+ }
22337
+ function isExternal(ref) {
22338
+ return /^[a-z][a-z0-9+.-]*:/i.test(ref) || ref.startsWith("//");
22339
+ }
22340
+ async function fetchAllMemoryFiles(auth) {
22341
+ const all = [];
22342
+ let offset = 0;
22343
+ const limit = 100;
22344
+ for (;; ) {
22345
+ const data = await amikoWebFetch(auth, "/api/memory-files", {
22346
+ query: { limit: String(limit), offset: String(offset) }
22347
+ });
22348
+ all.push(...data.items ?? []);
22349
+ if (!data.has_more || !data.items?.length)
22350
+ break;
22351
+ offset += data.items.length;
22352
+ }
22353
+ return all;
22354
+ }
22355
+
22015
22356
  // src/commands/wallets.ts
22016
22357
  function formatBalance(b) {
22017
22358
  const n = Number(b.amount) / 10 ** b.decimals;
@@ -22255,4 +22596,5 @@ registerReviewCommand(review);
22255
22596
  registerComposioCommand(composio);
22256
22597
  registerConversationCommand(conversation);
22257
22598
  registerNotificationsCommand(notifications);
22599
+ registerMemoryCommand(program2);
22258
22600
  program2.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyamiko/amiko-cli",
3
- "version": "0.9.8",
3
+ "version": "0.9.9-beta.1",
4
4
  "description": "Amiko CLI — swap tokens, manage credits, bridge cross-chain, and call marketplace agents",
5
5
  "type": "module",
6
6
  "bin": {