@memoryrelay/plugin-memoryrelay-ai 0.13.0 → 0.15.0

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/index.ts CHANGED
@@ -709,7 +709,7 @@ class MemoryRelayClient {
709
709
  headers: {
710
710
  "Content-Type": "application/json",
711
711
  Authorization: `Bearer ${this.apiKey}`,
712
- "User-Agent": "openclaw-memory-memoryrelay/0.13.0",
712
+ "User-Agent": "openclaw-memory-memoryrelay/0.15.0",
713
713
  },
714
714
  body: body ? JSON.stringify(body) : undefined,
715
715
  },
@@ -936,6 +936,63 @@ class MemoryRelayClient {
936
936
  });
937
937
  }
938
938
 
939
+ // --------------------------------------------------------------------------
940
+ // V2 Async API Methods (v0.15.0)
941
+ // --------------------------------------------------------------------------
942
+
943
+ async storeAsync(
944
+ content: string,
945
+ metadata?: Record<string, string>,
946
+ project?: string,
947
+ importance?: number,
948
+ tier?: string,
949
+ ): Promise<{ id: string; status: string; job_id: string; estimated_completion_seconds: number }> {
950
+ if (!content || content.length === 0 || content.length > 50000) {
951
+ throw new Error("Content must be between 1 and 50,000 characters");
952
+ }
953
+ const body: Record<string, unknown> = {
954
+ content,
955
+ agent_id: this.agentId,
956
+ };
957
+ if (metadata) body.metadata = metadata;
958
+ if (project) body.project = project;
959
+ if (importance != null) body.importance = importance;
960
+ if (tier) body.tier = tier;
961
+ return this.request("POST", "/v2/memories", body);
962
+ }
963
+
964
+ async getMemoryStatus(memoryId: string): Promise<{
965
+ id: string;
966
+ status: "pending" | "processing" | "ready" | "failed";
967
+ created_at: string;
968
+ updated_at: string;
969
+ error?: string;
970
+ }> {
971
+ return this.request("GET", `/v2/memories/${memoryId}/status`);
972
+ }
973
+
974
+ async buildContextV2(
975
+ query: string,
976
+ options?: {
977
+ maxMemories?: number;
978
+ maxTokens?: number;
979
+ aiEnhanced?: boolean;
980
+ searchMode?: "semantic" | "hybrid" | "keyword";
981
+ excludeMemoryIds?: string[];
982
+ },
983
+ ): Promise<any> {
984
+ const body: Record<string, unknown> = {
985
+ query,
986
+ agent_id: this.agentId,
987
+ };
988
+ if (options?.maxMemories != null) body.max_memories = options.maxMemories;
989
+ if (options?.maxTokens != null) body.max_tokens = options.maxTokens;
990
+ if (options?.aiEnhanced != null) body.ai_enhanced = options.aiEnhanced;
991
+ if (options?.searchMode) body.search_mode = options.searchMode;
992
+ if (options?.excludeMemoryIds) body.exclude_memory_ids = options.excludeMemoryIds;
993
+ return this.request("POST", "/v2/context", body);
994
+ }
995
+
939
996
  // --------------------------------------------------------------------------
940
997
  // Entity operations
941
998
  // --------------------------------------------------------------------------
@@ -1583,6 +1640,7 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
1583
1640
  "project_related", "project_impact", "project_shared_patterns", "project_context",
1584
1641
  ],
1585
1642
  health: ["memory_health"],
1643
+ v2: ["memory_store_async", "memory_status", "context_build"],
1586
1644
  };
1587
1645
 
1588
1646
  // Build a set of enabled tool names from group names
@@ -3746,6 +3804,202 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
3746
3804
  );
3747
3805
  }
3748
3806
 
3807
+ // 40. memory_store_async
3808
+ // --------------------------------------------------------------------------
3809
+ if (isToolEnabled("memory_store_async")) {
3810
+ api.registerTool((_ctx) => ({
3811
+ name: "memory_store_async",
3812
+ description:
3813
+ "Store a memory asynchronously using V2 API. Returns immediately (<50ms) with a job ID. Background workers generate the embedding. Use memory_status to poll for completion. Prefer this over memory_store for high-throughput or latency-sensitive applications." +
3814
+ (defaultProject ? ` Project defaults to '${defaultProject}' if not specified.` : ""),
3815
+ parameters: {
3816
+ type: "object",
3817
+ properties: {
3818
+ content: {
3819
+ type: "string",
3820
+ description: "The memory content to store (1–50,000 characters).",
3821
+ },
3822
+ metadata: {
3823
+ type: "object",
3824
+ description: "Optional key-value metadata to attach to the memory.",
3825
+ additionalProperties: { type: "string" },
3826
+ },
3827
+ project: {
3828
+ type: "string",
3829
+ description: "Project slug to associate with this memory (max 100 characters).",
3830
+ maxLength: 100,
3831
+ },
3832
+ importance: {
3833
+ type: "number",
3834
+ description: "Importance score (0-1). Higher values are retained longer.",
3835
+ minimum: 0,
3836
+ maximum: 1,
3837
+ },
3838
+ tier: {
3839
+ type: "string",
3840
+ description: "Memory tier: hot, warm, or cold.",
3841
+ enum: ["hot", "warm", "cold"],
3842
+ },
3843
+ },
3844
+ required: ["content"],
3845
+ },
3846
+ execute: async (
3847
+ _id,
3848
+ args: {
3849
+ content: string;
3850
+ metadata?: Record<string, string>;
3851
+ project?: string;
3852
+ importance?: number;
3853
+ tier?: string;
3854
+ },
3855
+ ) => {
3856
+ try {
3857
+ const { content, metadata, importance, tier } = args;
3858
+ let project = args.project;
3859
+ if (!project && defaultProject) project = defaultProject;
3860
+ const result = await client.storeAsync(content, metadata, project, importance, tier);
3861
+ return {
3862
+ content: [
3863
+ {
3864
+ type: "text",
3865
+ text: `Memory queued for async storage (id: ${result.id}, job_id: ${result.job_id}). Use memory_status to check completion.`,
3866
+ },
3867
+ ],
3868
+ details: result,
3869
+ };
3870
+ } catch (err) {
3871
+ return {
3872
+ content: [{ type: "text", text: `Failed to queue memory: ${String(err)}` }],
3873
+ details: { error: String(err) },
3874
+ };
3875
+ }
3876
+ },
3877
+ }), { name: "memory_store_async" });
3878
+ }
3879
+
3880
+ // 41. memory_status
3881
+ // --------------------------------------------------------------------------
3882
+ if (isToolEnabled("memory_status")) {
3883
+ api.registerTool((_ctx) => ({
3884
+ name: "memory_status",
3885
+ description:
3886
+ "Check the processing status of a memory created via memory_store_async. Status values: pending (waiting for worker), processing (generating embedding), ready (searchable), failed (error occurred).",
3887
+ parameters: {
3888
+ type: "object",
3889
+ properties: {
3890
+ memory_id: {
3891
+ type: "string",
3892
+ description: "The memory ID returned by memory_store_async.",
3893
+ },
3894
+ },
3895
+ required: ["memory_id"],
3896
+ },
3897
+ execute: async (
3898
+ _id,
3899
+ args: { memory_id: string },
3900
+ ) => {
3901
+ try {
3902
+ const status = await client.getMemoryStatus(args.memory_id);
3903
+ return {
3904
+ content: [
3905
+ {
3906
+ type: "text",
3907
+ text: JSON.stringify(status, null, 2),
3908
+ },
3909
+ ],
3910
+ details: status,
3911
+ };
3912
+ } catch (err) {
3913
+ return {
3914
+ content: [{ type: "text", text: `Failed to get memory status: ${String(err)}` }],
3915
+ details: { error: String(err) },
3916
+ };
3917
+ }
3918
+ },
3919
+ }), { name: "memory_status" });
3920
+ }
3921
+
3922
+ // 42. context_build
3923
+ // --------------------------------------------------------------------------
3924
+ if (isToolEnabled("context_build")) {
3925
+ api.registerTool((_ctx) => ({
3926
+ name: "context_build",
3927
+ description:
3928
+ "Build a ranked context bundle from memories with optional AI summarization. Searches for relevant memories, ranks them by composite score, and optionally generates an AI summary. Useful for building token-efficient context windows.",
3929
+ parameters: {
3930
+ type: "object",
3931
+ properties: {
3932
+ query: {
3933
+ type: "string",
3934
+ description: "The query to build context for.",
3935
+ },
3936
+ max_memories: {
3937
+ type: "number",
3938
+ description: "Maximum number of memories to include (1-100).",
3939
+ minimum: 1,
3940
+ maximum: 100,
3941
+ },
3942
+ max_tokens: {
3943
+ type: "number",
3944
+ description: "Maximum tokens for the context bundle (100-128000).",
3945
+ minimum: 100,
3946
+ maximum: 128000,
3947
+ },
3948
+ ai_enhanced: {
3949
+ type: "boolean",
3950
+ description: "If true, generate an AI summary of the retrieved memories.",
3951
+ },
3952
+ search_mode: {
3953
+ type: "string",
3954
+ description: "Search strategy: semantic, hybrid, or keyword.",
3955
+ enum: ["semantic", "hybrid", "keyword"],
3956
+ },
3957
+ exclude_memory_ids: {
3958
+ type: "array",
3959
+ description: "Memory IDs to exclude from results.",
3960
+ items: { type: "string" },
3961
+ },
3962
+ },
3963
+ required: ["query"],
3964
+ },
3965
+ execute: async (
3966
+ _id,
3967
+ args: {
3968
+ query: string;
3969
+ max_memories?: number;
3970
+ max_tokens?: number;
3971
+ ai_enhanced?: boolean;
3972
+ search_mode?: "semantic" | "hybrid" | "keyword";
3973
+ exclude_memory_ids?: string[];
3974
+ },
3975
+ ) => {
3976
+ try {
3977
+ const context = await client.buildContextV2(args.query, {
3978
+ maxMemories: args.max_memories,
3979
+ maxTokens: args.max_tokens,
3980
+ aiEnhanced: args.ai_enhanced,
3981
+ searchMode: args.search_mode,
3982
+ excludeMemoryIds: args.exclude_memory_ids,
3983
+ });
3984
+ return {
3985
+ content: [
3986
+ {
3987
+ type: "text",
3988
+ text: JSON.stringify(context, null, 2),
3989
+ },
3990
+ ],
3991
+ details: context,
3992
+ };
3993
+ } catch (err) {
3994
+ return {
3995
+ content: [{ type: "text", text: `Failed to build context: ${String(err)}` }],
3996
+ details: { error: String(err) },
3997
+ };
3998
+ }
3999
+ },
4000
+ }), { name: "context_build" });
4001
+ }
4002
+
3749
4003
  // ========================================================================
3750
4004
  // CLI Commands
3751
4005
  // ========================================================================
@@ -4236,7 +4490,7 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
4236
4490
  });
4237
4491
 
4238
4492
  api.logger.info?.(
4239
- `memory-memoryrelay: plugin v0.13.0 loaded (39 tools, autoRecall: ${cfg?.autoRecall}, autoCapture: ${autoCaptureConfig.enabled ? autoCaptureConfig.tier : 'off'}, debug: ${debugEnabled})`,
4493
+ `memory-memoryrelay: plugin v0.15.0 loaded (${Object.values(TOOL_GROUPS).flat().length} tools, autoRecall: ${cfg?.autoRecall}, autoCapture: ${autoCaptureConfig.enabled ? autoCaptureConfig.tier : 'off'}, debug: ${debugEnabled})`,
4240
4494
  );
4241
4495
 
4242
4496
  // ========================================================================
@@ -4586,7 +4840,65 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
4586
4840
  });
4587
4841
 
4588
4842
  // ========================================================================
4589
- // Direct Commands (5 total) — bypass LLM, execute immediately
4843
+ // Command Argument Parser (v0.14.0)
4844
+ // ========================================================================
4845
+
4846
+ function parseCommandArgs(input: string | undefined): { positional: string[]; flags: Record<string, string | boolean> } {
4847
+ const positional: string[] = [];
4848
+ const flags: Record<string, string | boolean> = {};
4849
+
4850
+ if (!input || input.trim() === "") {
4851
+ return { positional, flags };
4852
+ }
4853
+
4854
+ const tokens: string[] = [];
4855
+ let current = "";
4856
+ let inQuote: string | null = null;
4857
+
4858
+ for (const ch of input) {
4859
+ if (inQuote) {
4860
+ if (ch === inQuote) {
4861
+ inQuote = null;
4862
+ } else {
4863
+ current += ch;
4864
+ }
4865
+ } else if (ch === '"' || ch === "'") {
4866
+ inQuote = ch;
4867
+ } else if (ch === " " || ch === "\t") {
4868
+ if (current) {
4869
+ tokens.push(current);
4870
+ current = "";
4871
+ }
4872
+ } else {
4873
+ current += ch;
4874
+ }
4875
+ }
4876
+ if (current) tokens.push(current);
4877
+
4878
+ let i = 0;
4879
+ while (i < tokens.length) {
4880
+ const token = tokens[i];
4881
+ if (token.startsWith("--")) {
4882
+ const key = token.slice(2);
4883
+ const next = tokens[i + 1];
4884
+ if (next && !next.startsWith("--")) {
4885
+ flags[key] = next;
4886
+ i += 2;
4887
+ } else {
4888
+ flags[key] = true;
4889
+ i += 1;
4890
+ }
4891
+ } else {
4892
+ positional.push(token);
4893
+ i += 1;
4894
+ }
4895
+ }
4896
+
4897
+ return { positional, flags };
4898
+ }
4899
+
4900
+ // ========================================================================
4901
+ // Direct Commands (16 total) — bypass LLM, execute immediately
4590
4902
  // ========================================================================
4591
4903
 
4592
4904
  // /memory-status — Show full plugin status report
@@ -4789,6 +5101,485 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
4789
5101
  },
4790
5102
  });
4791
5103
 
5104
+ // ========================================================================
5105
+ // Direct Commands (10 new — v0.14.0)
5106
+ // ========================================================================
5107
+
5108
+ // /memory-search — Semantic memory search
5109
+ api.registerCommand?.({
5110
+ name: "memory-search",
5111
+ description: "Semantic search across stored memories",
5112
+ requireAuth: true,
5113
+ acceptsArgs: true,
5114
+ handler: async (ctx) => {
5115
+ try {
5116
+ const { positional, flags } = parseCommandArgs(ctx.args);
5117
+ const query = positional[0];
5118
+ if (!query) {
5119
+ return { text: "Usage: /memory-search <query> [--limit 10] [--project slug] [--threshold 0.3]" };
5120
+ }
5121
+ const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 10;
5122
+ const threshold = flags["threshold"] ? parseFloat(String(flags["threshold"])) : 0.3;
5123
+ const project = flags["project"] ? String(flags["project"]) : undefined;
5124
+
5125
+ const results = await client.search(query, limit, threshold, { project });
5126
+ const items: unknown[] = Array.isArray(results) ? results : (results as { data?: unknown[] }).data ?? [];
5127
+
5128
+ if (items.length === 0) {
5129
+ return { text: `No memories found for: "${query}"` };
5130
+ }
5131
+
5132
+ const lines: string[] = [`Memory Search: "${query}"`, "━".repeat(60)];
5133
+ for (const item of items) {
5134
+ const m = item as Record<string, unknown>;
5135
+ const content = String(m["content"] ?? "").slice(0, 120);
5136
+ const score = typeof m["similarity"] === "number" ? `${Math.round(m["similarity"] as number * 100)}%` : "N/A";
5137
+ const category = String(m["category"] ?? "general");
5138
+ const date = m["created_at"] ? new Date(String(m["created_at"])).toLocaleDateString() : "unknown";
5139
+ const id = String(m["id"] ?? "");
5140
+ lines.push(`[${score}] ${content}`);
5141
+ lines.push(` Category: ${category} | Date: ${date} | ID: ${id}`);
5142
+ }
5143
+
5144
+ return { text: lines.join("\n") };
5145
+ } catch (err) {
5146
+ return { text: `Error: ${String(err)}`, isError: true };
5147
+ }
5148
+ },
5149
+ });
5150
+
5151
+ // /memory-validate — Production readiness check
5152
+ api.registerCommand?.({
5153
+ name: "memory-validate",
5154
+ description: "Run production readiness checks for the MemoryRelay plugin",
5155
+ requireAuth: true,
5156
+ handler: async (_ctx) => {
5157
+ try {
5158
+ const results: Array<{ label: string; status: "PASS" | "FAIL" | "WARN"; detail: string }> = [];
5159
+
5160
+ // 1. API connectivity
5161
+ try {
5162
+ await client.health();
5163
+ results.push({ label: "API connectivity", status: "PASS", detail: "Health endpoint reachable" });
5164
+ } catch (err) {
5165
+ results.push({ label: "API connectivity", status: "FAIL", detail: String(err) });
5166
+ }
5167
+
5168
+ // 2. API health status
5169
+ try {
5170
+ const h = await client.health();
5171
+ const statusStr = String(h.status).toLowerCase();
5172
+ if (VALID_HEALTH_STATUSES.includes(statusStr)) {
5173
+ results.push({ label: "API health", status: "PASS", detail: `Status: ${h.status}` });
5174
+ } else {
5175
+ results.push({ label: "API health", status: "WARN", detail: `Unexpected status: ${h.status}` });
5176
+ }
5177
+ } catch (err) {
5178
+ results.push({ label: "API health", status: "FAIL", detail: String(err) });
5179
+ }
5180
+
5181
+ // 3. Core tools
5182
+ const allTools = Object.values(TOOL_GROUPS).flat();
5183
+ const coreTools = ["memory_store", "memory_recall", "memory_list"];
5184
+ const missing = coreTools.filter((t) => !allTools.includes(t));
5185
+ if (missing.length === 0) {
5186
+ results.push({ label: "Core tools", status: "PASS", detail: "memory_store, memory_recall, memory_list present" });
5187
+ } else {
5188
+ results.push({ label: "Core tools", status: "FAIL", detail: `Missing: ${missing.join(", ")}` });
5189
+ }
5190
+
5191
+ // 4. Auto-recall enabled
5192
+ const autoRecall = cfg?.autoRecall ?? true;
5193
+ results.push({
5194
+ label: "Auto-recall enabled",
5195
+ status: autoRecall ? "PASS" : "WARN",
5196
+ detail: autoRecall ? "Enabled" : "Disabled in config",
5197
+ });
5198
+
5199
+ // 5. Auto-capture enabled
5200
+ results.push({
5201
+ label: "Auto-capture enabled",
5202
+ status: autoCaptureConfig.enabled ? "PASS" : "WARN",
5203
+ detail: autoCaptureConfig.enabled ? `Enabled (tier: ${autoCaptureConfig.tier})` : "Disabled in config",
5204
+ });
5205
+
5206
+ // 6. Memory storage
5207
+ try {
5208
+ await client.list(1);
5209
+ results.push({ label: "Memory storage", status: "PASS", detail: "Storage accessible" });
5210
+ } catch (err) {
5211
+ results.push({ label: "Memory storage", status: "FAIL", detail: String(err) });
5212
+ }
5213
+
5214
+ // 7. Agent ID configured
5215
+ const agentIdOk = agentId && agentId !== "" && agentId !== "default";
5216
+ results.push({
5217
+ label: "Agent ID configured",
5218
+ status: agentIdOk ? "PASS" : "WARN",
5219
+ detail: agentIdOk ? `ID: ${agentId}` : `Agent ID is "${agentId}" — consider setting a unique ID`,
5220
+ });
5221
+
5222
+ const passes = results.filter((r) => r.status === "PASS").length;
5223
+ const failures = results.filter((r) => r.status === "FAIL").length;
5224
+ let grade: string;
5225
+ if (passes === 7) grade = "A+";
5226
+ else if (passes === 6) grade = "A";
5227
+ else if (passes === 5) grade = "B+";
5228
+ else if (passes === 4) grade = "B";
5229
+ else grade = "F";
5230
+
5231
+ const lines: string[] = ["MemoryRelay Production Readiness", "━".repeat(50)];
5232
+ for (const r of results) {
5233
+ lines.push(`[${r.status.padEnd(4)}] ${r.label}: ${r.detail}`);
5234
+ }
5235
+ lines.push("─".repeat(50));
5236
+ lines.push(`Checks passed: ${passes}/7 | Grade: ${grade} | Production ready: ${failures === 0 ? "Yes" : "No"}`);
5237
+
5238
+ return { text: lines.join("\n") };
5239
+ } catch (err) {
5240
+ return { text: `Error: ${String(err)}`, isError: true };
5241
+ }
5242
+ },
5243
+ });
5244
+
5245
+ // /memory-config — Read-only config display
5246
+ api.registerCommand?.({
5247
+ name: "memory-config",
5248
+ description: "Display current MemoryRelay plugin configuration",
5249
+ requireAuth: true,
5250
+ handler: async (_ctx) => {
5251
+ try {
5252
+ const lines: string[] = ["MemoryRelay Configuration", "━".repeat(50)];
5253
+ lines.push(`API URL: ${apiUrl}`);
5254
+ lines.push(`Agent ID: ${agentId}`);
5255
+ lines.push(`Default Project: ${defaultProject || "(none)"}`);
5256
+ lines.push(`Enabled Tools: ${cfg?.enabledTools ?? "all"}`);
5257
+ lines.push(`Auto-Recall: ${cfg?.autoRecall ?? true}`);
5258
+ lines.push(`Auto-Capture: ${autoCaptureConfig.enabled} (tier: ${autoCaptureConfig.tier})`);
5259
+ lines.push(`Recall Limit: ${cfg?.recallLimit ?? 5}`);
5260
+ lines.push(`Recall Threshold: ${cfg?.recallThreshold ?? 0.3}`);
5261
+ lines.push(`Exclude Channels: ${(cfg?.excludeChannels ?? []).join(", ") || "(none)"}`);
5262
+ lines.push(`Session Timeout: ${cfg?.sessionTimeoutMinutes ?? 120} min`);
5263
+ lines.push(`Cleanup Interval: ${cfg?.sessionCleanupIntervalMinutes ?? 30} min`);
5264
+ lines.push(`Debug: ${cfg?.debug ?? false}`);
5265
+ lines.push(`Verbose: ${cfg?.verbose ?? false}`);
5266
+ lines.push(`Max Log Entries: ${cfg?.maxLogEntries ?? 100}`);
5267
+ return { text: lines.join("\n") };
5268
+ } catch (err) {
5269
+ return { text: `Error: ${String(err)}`, isError: true };
5270
+ }
5271
+ },
5272
+ });
5273
+
5274
+ // /memory-sessions — List sessions
5275
+ api.registerCommand?.({
5276
+ name: "memory-sessions",
5277
+ description: "List MemoryRelay sessions",
5278
+ requireAuth: true,
5279
+ acceptsArgs: true,
5280
+ handler: async (ctx) => {
5281
+ try {
5282
+ const { flags } = parseCommandArgs(ctx.args);
5283
+ const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 10;
5284
+ const project = flags["project"] ? String(flags["project"]) : undefined;
5285
+ let status: string | undefined = flags["status"] ? String(flags["status"]) : undefined;
5286
+ if (flags["active"]) status = "active";
5287
+
5288
+ const raw = await client.listSessions(limit, project, status);
5289
+ const sessions: unknown[] = Array.isArray(raw) ? raw : (raw as { data?: unknown[] }).data ?? [];
5290
+
5291
+ if (sessions.length === 0) {
5292
+ return { text: "No sessions found." };
5293
+ }
5294
+
5295
+ const lines: string[] = ["MemoryRelay Sessions", "━".repeat(60)];
5296
+ for (const session of sessions) {
5297
+ const s = session as Record<string, unknown>;
5298
+ const sid = String(s["id"] ?? "");
5299
+ const sessionStatus = String(s["status"] ?? "unknown").toUpperCase();
5300
+ const startedAt = s["started_at"] ? new Date(String(s["started_at"])).toLocaleString() : "unknown";
5301
+ let duration = "ongoing";
5302
+ if (s["started_at"] && s["ended_at"]) {
5303
+ const diffMs = new Date(String(s["ended_at"])).getTime() - new Date(String(s["started_at"])).getTime();
5304
+ const diffMin = Math.round(diffMs / 60000);
5305
+ duration = `${diffMin}m`;
5306
+ }
5307
+ const summary = String(s["summary"] ?? "").slice(0, 80);
5308
+ lines.push(`[${sessionStatus}] ${sid}`);
5309
+ lines.push(` Started: ${startedAt} | Duration: ${duration}`);
5310
+ if (summary) lines.push(` ${summary}`);
5311
+ }
5312
+
5313
+ return { text: lines.join("\n") };
5314
+ } catch (err) {
5315
+ return { text: `Error: ${String(err)}`, isError: true };
5316
+ }
5317
+ },
5318
+ });
5319
+
5320
+ // /memory-decisions — List decisions
5321
+ api.registerCommand?.({
5322
+ name: "memory-decisions",
5323
+ description: "List architectural decisions stored in MemoryRelay",
5324
+ requireAuth: true,
5325
+ acceptsArgs: true,
5326
+ handler: async (ctx) => {
5327
+ try {
5328
+ const { flags } = parseCommandArgs(ctx.args);
5329
+ const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 10;
5330
+ const project = flags["project"] ? String(flags["project"]) : undefined;
5331
+ const status = flags["status"] ? String(flags["status"]) : undefined;
5332
+ const tags = flags["tags"] ? String(flags["tags"]) : undefined;
5333
+
5334
+ const raw = await client.listDecisions(limit, project, status, tags);
5335
+ const decisions: unknown[] = Array.isArray(raw) ? raw : (raw as { data?: unknown[] }).data ?? [];
5336
+
5337
+ if (decisions.length === 0) {
5338
+ return { text: "No decisions found." };
5339
+ }
5340
+
5341
+ const lines: string[] = ["MemoryRelay Decisions", "━".repeat(60)];
5342
+ for (const decision of decisions) {
5343
+ const d = decision as Record<string, unknown>;
5344
+ const decisionStatus = String(d["status"] ?? "unknown").toUpperCase();
5345
+ const title = String(d["title"] ?? "(untitled)");
5346
+ const date = d["created_at"] ? new Date(String(d["created_at"])).toLocaleDateString() : "unknown";
5347
+ const rationale = String(d["rationale"] ?? "").slice(0, 100);
5348
+ lines.push(`[${decisionStatus}] ${title} (${date})`);
5349
+ if (rationale) lines.push(` ${rationale}`);
5350
+ }
5351
+
5352
+ return { text: lines.join("\n") };
5353
+ } catch (err) {
5354
+ return { text: `Error: ${String(err)}`, isError: true };
5355
+ }
5356
+ },
5357
+ });
5358
+
5359
+ // /memory-patterns — List/search patterns
5360
+ api.registerCommand?.({
5361
+ name: "memory-patterns",
5362
+ description: "List or search memory patterns",
5363
+ requireAuth: true,
5364
+ acceptsArgs: true,
5365
+ handler: async (ctx) => {
5366
+ try {
5367
+ const { positional, flags } = parseCommandArgs(ctx.args);
5368
+ const query = positional[0] ?? "";
5369
+ const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 10;
5370
+ const category = flags["category"] ? String(flags["category"]) : undefined;
5371
+ const project = flags["project"] ? String(flags["project"]) : undefined;
5372
+
5373
+ const raw = await client.searchPatterns(query, category, project, limit);
5374
+ const patterns: unknown[] = Array.isArray(raw) ? raw : (raw as { data?: unknown[] }).data ?? [];
5375
+
5376
+ if (patterns.length === 0) {
5377
+ return { text: query ? `No patterns found for: "${query}"` : "No patterns found." };
5378
+ }
5379
+
5380
+ const lines: string[] = ["MemoryRelay Patterns", "━".repeat(60)];
5381
+ for (const pattern of patterns) {
5382
+ const p = pattern as Record<string, unknown>;
5383
+ const name = String(p["name"] ?? "(unnamed)");
5384
+ const cat = String(p["category"] ?? "general");
5385
+ const description = String(p["description"] ?? "").slice(0, 100);
5386
+ lines.push(`${name} [${cat}]`);
5387
+ if (description) lines.push(` ${description}`);
5388
+ }
5389
+
5390
+ return { text: lines.join("\n") };
5391
+ } catch (err) {
5392
+ return { text: `Error: ${String(err)}`, isError: true };
5393
+ }
5394
+ },
5395
+ });
5396
+
5397
+ // /memory-entities — List entities
5398
+ api.registerCommand?.({
5399
+ name: "memory-entities",
5400
+ description: "List entities stored in MemoryRelay",
5401
+ requireAuth: true,
5402
+ acceptsArgs: true,
5403
+ handler: async (ctx) => {
5404
+ try {
5405
+ const { flags } = parseCommandArgs(ctx.args);
5406
+ const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 20;
5407
+
5408
+ const raw = await client.listEntities(limit);
5409
+ const entities: unknown[] = Array.isArray(raw) ? raw : (raw as { data?: unknown[] }).data ?? [];
5410
+
5411
+ if (entities.length === 0) {
5412
+ return { text: "No entities found." };
5413
+ }
5414
+
5415
+ const lines: string[] = ["MemoryRelay Entities", "━".repeat(60)];
5416
+ for (const entity of entities) {
5417
+ const e = entity as Record<string, unknown>;
5418
+ const name = String(e["name"] ?? "(unnamed)");
5419
+ const type = String(e["type"] ?? "unknown");
5420
+ const relationships = Array.isArray(e["relationships"]) ? e["relationships"].length : (typeof e["relationship_count"] === "number" ? e["relationship_count"] : 0);
5421
+ lines.push(`${name} [${type}] (${relationships} relationships)`);
5422
+ }
5423
+
5424
+ return { text: lines.join("\n") };
5425
+ } catch (err) {
5426
+ return { text: `Error: ${String(err)}`, isError: true };
5427
+ }
5428
+ },
5429
+ });
5430
+
5431
+ // /memory-projects — List projects
5432
+ api.registerCommand?.({
5433
+ name: "memory-projects",
5434
+ description: "List projects in MemoryRelay",
5435
+ requireAuth: true,
5436
+ acceptsArgs: true,
5437
+ handler: async (ctx) => {
5438
+ try {
5439
+ const { flags } = parseCommandArgs(ctx.args);
5440
+ const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 20;
5441
+
5442
+ const raw = await client.listProjects(limit);
5443
+ const projects: unknown[] = Array.isArray(raw) ? raw : (raw as { data?: unknown[] }).data ?? [];
5444
+
5445
+ if (projects.length === 0) {
5446
+ return { text: "No projects found." };
5447
+ }
5448
+
5449
+ const lines: string[] = ["MemoryRelay Projects", "━".repeat(60)];
5450
+ for (const project of projects) {
5451
+ const p = project as Record<string, unknown>;
5452
+ const slug = String(p["slug"] ?? "(no-slug)");
5453
+ const description = String(p["description"] ?? "").slice(0, 80);
5454
+ const memoryCount = typeof p["memory_count"] === "number" ? p["memory_count"] : 0;
5455
+ lines.push(`${slug} — ${description || "(no description)"} (${memoryCount} memories)`);
5456
+ }
5457
+
5458
+ return { text: lines.join("\n") };
5459
+ } catch (err) {
5460
+ return { text: `Error: ${String(err)}`, isError: true };
5461
+ }
5462
+ },
5463
+ });
5464
+
5465
+ // /memory-agents — List agents
5466
+ api.registerCommand?.({
5467
+ name: "memory-agents",
5468
+ description: "List agents registered in MemoryRelay",
5469
+ requireAuth: true,
5470
+ acceptsArgs: true,
5471
+ handler: async (ctx) => {
5472
+ try {
5473
+ const { flags } = parseCommandArgs(ctx.args);
5474
+ const limit = flags["limit"] ? parseInt(String(flags["limit"]), 10) : 20;
5475
+
5476
+ const raw = await client.listAgents(limit);
5477
+ const agents: unknown[] = Array.isArray(raw) ? raw : (raw as { data?: unknown[] }).data ?? [];
5478
+
5479
+ if (agents.length === 0) {
5480
+ return { text: "No agents found." };
5481
+ }
5482
+
5483
+ const lines: string[] = ["MemoryRelay Agents", "━".repeat(60)];
5484
+ for (const agent of agents) {
5485
+ const a = agent as Record<string, unknown>;
5486
+ const id = String(a["id"] ?? "(no-id)");
5487
+ const name = String(a["name"] ?? "");
5488
+ const description = String(a["description"] ?? "");
5489
+ lines.push(`${id}${name ? ` (${name})` : ""}${description ? `, ${description}` : ""}`);
5490
+ }
5491
+
5492
+ return { text: lines.join("\n") };
5493
+ } catch (err) {
5494
+ return { text: `Error: ${String(err)}`, isError: true };
5495
+ }
5496
+ },
5497
+ });
5498
+
5499
+ // /memory-forget — Delete a memory by ID
5500
+ api.registerCommand?.({
5501
+ name: "memory-forget",
5502
+ description: "Delete a specific memory by ID",
5503
+ requireAuth: true,
5504
+ acceptsArgs: true,
5505
+ handler: async (ctx) => {
5506
+ const { positional } = parseCommandArgs(ctx.args);
5507
+ const memoryId = positional[0];
5508
+ if (!memoryId) {
5509
+ return { text: "Usage: /memory-forget <memory-id>" };
5510
+ }
5511
+ try {
5512
+ let preview = "";
5513
+ try {
5514
+ const existing = await client.get(memoryId);
5515
+ const m = existing as Record<string, unknown>;
5516
+ preview = String(m["content"] ?? "").slice(0, 120);
5517
+ } catch (_) {
5518
+ // preview unavailable — proceed with delete
5519
+ }
5520
+
5521
+ await client.delete(memoryId);
5522
+
5523
+ const lines = [`Memory deleted: ${memoryId}`];
5524
+ if (preview) lines.push(`Content: ${preview}`);
5525
+ return { text: lines.join("\n") };
5526
+ } catch (err) {
5527
+ const msg = String(err);
5528
+ if (msg.toLowerCase().includes("not found") || msg.includes("404")) {
5529
+ return { text: `Memory not found: ${memoryId}`, isError: true };
5530
+ }
5531
+ return { text: `Error: ${msg}`, isError: true };
5532
+ }
5533
+ },
5534
+ });
5535
+
5536
+ // /memory-context — Build a context bundle from memories using V2 API
5537
+ api.registerCommand?.({
5538
+ name: "memory-context",
5539
+ description: "Build a ranked context bundle from memories for a given query",
5540
+ requireAuth: true,
5541
+ acceptsArgs: true,
5542
+ handler: async (ctx) => {
5543
+ const { positional, flags } = parseCommandArgs(ctx.args);
5544
+ const query = positional.join(" ").trim();
5545
+ if (!query) {
5546
+ return {
5547
+ text: [
5548
+ "Usage: /memory-context <query> [options]",
5549
+ "",
5550
+ "Options:",
5551
+ " --max-memories <n> Maximum memories to include (1-100)",
5552
+ " --max-tokens <n> Maximum tokens for context (100-128000)",
5553
+ " --ai-enhanced Generate an AI summary of memories",
5554
+ " --search-mode <mode> Search strategy: semantic, hybrid, keyword",
5555
+ ].join("\n"),
5556
+ };
5557
+ }
5558
+ try {
5559
+ const options: {
5560
+ maxMemories?: number;
5561
+ maxTokens?: number;
5562
+ aiEnhanced?: boolean;
5563
+ searchMode?: "semantic" | "hybrid" | "keyword";
5564
+ } = {};
5565
+ if (flags["max-memories"]) options.maxMemories = parseInt(String(flags["max-memories"]), 10);
5566
+ if (flags["max-tokens"]) options.maxTokens = parseInt(String(flags["max-tokens"]), 10);
5567
+ if (flags["ai-enhanced"] === true) options.aiEnhanced = true;
5568
+ if (flags["search-mode"]) options.searchMode = String(flags["search-mode"]) as "semantic" | "hybrid" | "keyword";
5569
+
5570
+ const context = await client.buildContextV2(query, options);
5571
+
5572
+ if (!context || (Array.isArray(context.memories) && context.memories.length === 0)) {
5573
+ return { text: `No memories found for query: "${query}"` };
5574
+ }
5575
+
5576
+ return { text: JSON.stringify(context, null, 2) };
5577
+ } catch (err) {
5578
+ return { text: `Error: ${String(err)}`, isError: true };
5579
+ }
5580
+ },
5581
+ });
5582
+
4792
5583
  // ========================================================================
4793
5584
  // Stale Session Cleanup Service (v0.13.0)
4794
5585
  // ========================================================================