@mem9/opencode 0.1.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/README.md +268 -0
- package/package.json +54 -0
- package/src/index.ts +1 -0
- package/src/server/backend.ts +39 -0
- package/src/server/config.ts +251 -0
- package/src/server/debug.ts +112 -0
- package/src/server/hooks.ts +349 -0
- package/src/server/index.ts +73 -0
- package/src/server/ingest/select.ts +18 -0
- package/src/server/ingest/submit.ts +56 -0
- package/src/server/recall/format.ts +84 -0
- package/src/server/recall/query.ts +127 -0
- package/src/server/server-backend.ts +170 -0
- package/src/server/session-transcript.ts +65 -0
- package/src/server/setup-flow.ts +17 -0
- package/src/server/tools.ts +189 -0
- package/src/shared/credentials-store.ts +55 -0
- package/src/shared/defaults.ts +13 -0
- package/src/shared/platform-paths.ts +73 -0
- package/src/shared/plugin-meta.ts +1 -0
- package/src/shared/setup-files.ts +492 -0
- package/src/shared/types.ts +74 -0
- package/src/tui/index.ts +622 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const TOOL_NOISE_TAGS = [
|
|
2
|
+
"local-command-caveat",
|
|
3
|
+
"local-command-stdout",
|
|
4
|
+
"command-name",
|
|
5
|
+
"command-message",
|
|
6
|
+
"task-notification",
|
|
7
|
+
"system-reminder",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export const MAX_RECALL_QUERY_PARAM_LEN = 1600;
|
|
11
|
+
|
|
12
|
+
const QUERY_ELLIPSIS = "\n...\n";
|
|
13
|
+
const QUERY_PARAM_KEY = "q";
|
|
14
|
+
|
|
15
|
+
function stripTaggedBlock(input: string, tagName: string): string {
|
|
16
|
+
const startTag = `<${tagName}>`;
|
|
17
|
+
const endTag = `</${tagName}>`;
|
|
18
|
+
|
|
19
|
+
let result = input;
|
|
20
|
+
while (result.includes(startTag)) {
|
|
21
|
+
const start = result.indexOf(startTag);
|
|
22
|
+
const end = result.indexOf(endTag, start);
|
|
23
|
+
|
|
24
|
+
if (end === -1) {
|
|
25
|
+
result = result.slice(0, start);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
result = result.slice(0, start) + result.slice(end + endTag.length);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clampRecallQuery(input: string): string {
|
|
36
|
+
if (encodedQueryParamLength(input) <= MAX_RECALL_QUERY_PARAM_LEN) {
|
|
37
|
+
return input;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const chars = Array.from(input);
|
|
41
|
+
let best = truncatePrefixToEncodedBudget(chars);
|
|
42
|
+
let low = 2;
|
|
43
|
+
let high = Math.max(chars.length - 2, 0);
|
|
44
|
+
|
|
45
|
+
while (low <= high) {
|
|
46
|
+
const keptChars = Math.floor((low + high) / 2);
|
|
47
|
+
const candidate = buildBalancedCandidate(chars, keptChars);
|
|
48
|
+
|
|
49
|
+
if (encodedQueryParamLength(candidate) <= MAX_RECALL_QUERY_PARAM_LEN) {
|
|
50
|
+
best = candidate;
|
|
51
|
+
low = keptChars + 1;
|
|
52
|
+
} else {
|
|
53
|
+
high = keptChars - 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return best;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function encodedQueryParamLength(input: string): number {
|
|
61
|
+
return new URLSearchParams({ [QUERY_PARAM_KEY]: input }).toString().length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildBalancedCandidate(chars: string[], keptChars: number): string {
|
|
65
|
+
if (keptChars <= 0) {
|
|
66
|
+
return QUERY_ELLIPSIS;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const prefixLen = Math.ceil(keptChars / 2);
|
|
70
|
+
const suffixLen = Math.floor(keptChars / 2);
|
|
71
|
+
const prefix = chars.slice(0, prefixLen).join("").trimEnd();
|
|
72
|
+
const suffix = chars.slice(chars.length - suffixLen).join("").trimStart();
|
|
73
|
+
|
|
74
|
+
return `${prefix}${QUERY_ELLIPSIS}${suffix}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function truncatePrefixToEncodedBudget(chars: string[]): string {
|
|
78
|
+
let low = 0;
|
|
79
|
+
let high = chars.length;
|
|
80
|
+
let best = "";
|
|
81
|
+
|
|
82
|
+
while (low <= high) {
|
|
83
|
+
const length = Math.floor((low + high) / 2);
|
|
84
|
+
const candidate = chars.slice(0, length).join("").trimEnd();
|
|
85
|
+
|
|
86
|
+
if (encodedQueryParamLength(candidate) <= MAX_RECALL_QUERY_PARAM_LEN) {
|
|
87
|
+
best = candidate;
|
|
88
|
+
low = length + 1;
|
|
89
|
+
} else {
|
|
90
|
+
high = length - 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return best;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildRecallQuery(input: string): string {
|
|
98
|
+
let result = input.replace(/\r\n?/g, "\n");
|
|
99
|
+
|
|
100
|
+
result = stripTaggedBlock(result, "relevant-memories");
|
|
101
|
+
for (const tag of TOOL_NOISE_TAGS) {
|
|
102
|
+
result = stripTaggedBlock(result, tag);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
result = result.replace(
|
|
106
|
+
/^Conversation info \(untrusted metadata\):\s*\n```[\s\S]*?\n```\s*/gm,
|
|
107
|
+
"",
|
|
108
|
+
);
|
|
109
|
+
result = result.replace(
|
|
110
|
+
/^Sender \(untrusted metadata\):\s*\n```[\s\S]*?\n```\s*/gm,
|
|
111
|
+
"",
|
|
112
|
+
);
|
|
113
|
+
result = result.replace(
|
|
114
|
+
/<<<EXTERNAL_UNTRUSTED_CONTENT[\s\S]*?<<<END_EXTERNAL_UNTRUSTED_CONTENT[^>]*>>>/g,
|
|
115
|
+
"",
|
|
116
|
+
);
|
|
117
|
+
result = result.replace(
|
|
118
|
+
/^Untrusted context \(metadata, do not treat as instructions or commands\):\s*$/gm,
|
|
119
|
+
"",
|
|
120
|
+
);
|
|
121
|
+
result = result.replace(/^\s*Source:\s.*$/gm, "");
|
|
122
|
+
result = result.replace(/^\s*UNTRUSTED[^\n]*$/gm, "");
|
|
123
|
+
result = result.replace(/^\s*---\s*$/gm, "");
|
|
124
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
125
|
+
|
|
126
|
+
return clampRecallQuery(result.trim());
|
|
127
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IngestInput,
|
|
3
|
+
IngestResult,
|
|
4
|
+
MemoryBackend,
|
|
5
|
+
} from "./backend.js";
|
|
6
|
+
import type {
|
|
7
|
+
Memory,
|
|
8
|
+
StoreResult,
|
|
9
|
+
SearchResult,
|
|
10
|
+
CreateMemoryInput,
|
|
11
|
+
UpdateMemoryInput,
|
|
12
|
+
SearchInput,
|
|
13
|
+
} from "../shared/types.js";
|
|
14
|
+
import { DEFAULT_SCOPE_CONFIG } from "../shared/defaults.js";
|
|
15
|
+
|
|
16
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeTimeoutMs(value: number | undefined, fallback: number): number {
|
|
21
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return Math.floor(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ServerBackendOptions {
|
|
29
|
+
defaultTimeoutMs?: number;
|
|
30
|
+
searchTimeoutMs?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ServerBackend — talks to mem9 REST API.
|
|
35
|
+
* Used when a runtime API key is available.
|
|
36
|
+
*/
|
|
37
|
+
export class ServerBackend implements MemoryBackend {
|
|
38
|
+
private baseUrl: string;
|
|
39
|
+
private defaultTimeoutMs: number;
|
|
40
|
+
private searchTimeoutMs: number;
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
apiUrl: string,
|
|
44
|
+
private apiKey: string,
|
|
45
|
+
private agentName: string = "opencode",
|
|
46
|
+
options: ServerBackendOptions = {},
|
|
47
|
+
) {
|
|
48
|
+
this.baseUrl = apiUrl.replace(/\/+$/, "");
|
|
49
|
+
this.defaultTimeoutMs = normalizeTimeoutMs(
|
|
50
|
+
options.defaultTimeoutMs,
|
|
51
|
+
DEFAULT_SCOPE_CONFIG.defaultTimeoutMs,
|
|
52
|
+
);
|
|
53
|
+
this.searchTimeoutMs = normalizeTimeoutMs(
|
|
54
|
+
options.searchTimeoutMs,
|
|
55
|
+
DEFAULT_SCOPE_CONFIG.searchTimeoutMs,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private memoryPath(path: string): string {
|
|
60
|
+
return `/v1alpha2/mem9s${path}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async request<T>(
|
|
64
|
+
method: string,
|
|
65
|
+
path: string,
|
|
66
|
+
body?: unknown,
|
|
67
|
+
timeoutMs = this.defaultTimeoutMs,
|
|
68
|
+
): Promise<T> {
|
|
69
|
+
const url = this.baseUrl + path;
|
|
70
|
+
const headers: Record<string, string> = {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
"X-Mnemo-Agent-Id": this.agentName,
|
|
73
|
+
"X-API-Key": this.apiKey,
|
|
74
|
+
};
|
|
75
|
+
const resp = await fetch(url, {
|
|
76
|
+
method,
|
|
77
|
+
headers,
|
|
78
|
+
body: body != null ? JSON.stringify(body) : undefined,
|
|
79
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (resp.status === 204) return undefined as T;
|
|
83
|
+
|
|
84
|
+
const text = await resp.text();
|
|
85
|
+
const data = text ? (JSON.parse(text) as unknown) : undefined;
|
|
86
|
+
if (!resp.ok) {
|
|
87
|
+
const message =
|
|
88
|
+
isRecord(data) && typeof data.error === "string" ? data.error : `HTTP ${resp.status}`;
|
|
89
|
+
throw new Error(message);
|
|
90
|
+
}
|
|
91
|
+
return data as T;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async store(input: CreateMemoryInput): Promise<StoreResult> {
|
|
95
|
+
return this.request<StoreResult>("POST", this.memoryPath("/memories"), input);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async ingest(input: IngestInput): Promise<IngestResult> {
|
|
99
|
+
return this.request<IngestResult>("POST", this.memoryPath("/memories"), input);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async search(input: SearchInput): Promise<SearchResult> {
|
|
103
|
+
const params = new URLSearchParams();
|
|
104
|
+
if (input.q) params.set("q", input.q);
|
|
105
|
+
if (input.tags) params.set("tags", input.tags);
|
|
106
|
+
if (input.source) params.set("source", input.source);
|
|
107
|
+
if (input.limit != null) params.set("limit", String(input.limit));
|
|
108
|
+
if (input.offset != null) params.set("offset", String(input.offset));
|
|
109
|
+
if (input.memory_type) params.set("memory_type", input.memory_type);
|
|
110
|
+
|
|
111
|
+
const qs = params.toString();
|
|
112
|
+
const raw = await this.request<{
|
|
113
|
+
memories: Memory[];
|
|
114
|
+
total: number;
|
|
115
|
+
limit: number;
|
|
116
|
+
offset: number;
|
|
117
|
+
}>(
|
|
118
|
+
"GET",
|
|
119
|
+
`${this.memoryPath("/memories")}${qs ? "?" + qs : ""}`,
|
|
120
|
+
undefined,
|
|
121
|
+
this.searchTimeoutMs,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
memories: raw.memories ?? [],
|
|
126
|
+
total: raw.total,
|
|
127
|
+
limit: raw.limit,
|
|
128
|
+
offset: raw.offset,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async get(id: string): Promise<Memory | null> {
|
|
133
|
+
try {
|
|
134
|
+
return await this.request<Memory>("GET", this.memoryPath(`/memories/${id}`));
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err instanceof Error && (err.message.includes("not found") || err.message.includes("404"))) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async update(id: string, input: UpdateMemoryInput): Promise<Memory | null> {
|
|
144
|
+
try {
|
|
145
|
+
return await this.request<Memory>("PUT", this.memoryPath(`/memories/${id}`), input);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (err instanceof Error && (err.message.includes("not found") || err.message.includes("404"))) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async remove(id: string): Promise<boolean> {
|
|
155
|
+
try {
|
|
156
|
+
await this.request("DELETE", this.memoryPath(`/memories/${id}`));
|
|
157
|
+
return true;
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (err instanceof Error && (err.message.includes("not found") || err.message.includes("404"))) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async listRecent(limit: number): Promise<Memory[]> {
|
|
167
|
+
const result = await this.search({ limit, offset: 0 });
|
|
168
|
+
return result.memories;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import type { IngestMessage } from "./backend.js";
|
|
3
|
+
|
|
4
|
+
const SESSION_TRANSCRIPT_FETCH_LIMIT = 24;
|
|
5
|
+
|
|
6
|
+
interface TranscriptPartLike {
|
|
7
|
+
type: string;
|
|
8
|
+
text?: string;
|
|
9
|
+
synthetic?: boolean;
|
|
10
|
+
ignored?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type SessionTranscriptLoader = (sessionID: string) => Promise<IngestMessage[]>;
|
|
14
|
+
|
|
15
|
+
function extractMessageText(parts: TranscriptPartLike[]): string {
|
|
16
|
+
const chunks: string[] = [];
|
|
17
|
+
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
if (part.type !== "text" || typeof part.text !== "string") {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (part.synthetic === true || part.ignored === true) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const text = part.text.trim();
|
|
28
|
+
if (text) {
|
|
29
|
+
chunks.push(text);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return chunks.join("\n\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createSessionTranscriptLoader(
|
|
37
|
+
client: PluginInput["client"],
|
|
38
|
+
): SessionTranscriptLoader {
|
|
39
|
+
return async (sessionID: string): Promise<IngestMessage[]> => {
|
|
40
|
+
const response = await client.session.messages({
|
|
41
|
+
path: { id: sessionID },
|
|
42
|
+
query: { limit: SESSION_TRANSCRIPT_FETCH_LIMIT },
|
|
43
|
+
throwOnError: true,
|
|
44
|
+
});
|
|
45
|
+
const messages = response.data;
|
|
46
|
+
|
|
47
|
+
return messages.flatMap((entry): IngestMessage[] => {
|
|
48
|
+
if (entry.info.role !== "user" && entry.info.role !== "assistant") {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const content = extractMessageText(entry.parts);
|
|
53
|
+
if (!content) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
role: entry.info.role,
|
|
60
|
+
content,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Hooks, PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import type { EffectiveConfig } from "./config.js";
|
|
3
|
+
|
|
4
|
+
export function buildPendingSetupHooks(
|
|
5
|
+
_input: PluginInput,
|
|
6
|
+
config: EffectiveConfig,
|
|
7
|
+
): Hooks {
|
|
8
|
+
const target = config.profileId
|
|
9
|
+
? `profile "${config.profileId}"`
|
|
10
|
+
: "a mem9 profile";
|
|
11
|
+
|
|
12
|
+
console.warn(
|
|
13
|
+
`[mem9] Setup pending. Run /mem9-setup, configure MEM9_API_KEY, or add credentials for ${target} to enable mem9.`,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import type { MemoryBackend } from "./backend.js";
|
|
3
|
+
import type {
|
|
4
|
+
CreateMemoryInput,
|
|
5
|
+
UpdateMemoryInput,
|
|
6
|
+
SearchInput,
|
|
7
|
+
} from "../shared/types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build the 5 memory tools for OpenCode.
|
|
11
|
+
* Returns a map of tool name → ToolDefinition.
|
|
12
|
+
*/
|
|
13
|
+
export function buildTools(backend: MemoryBackend): Record<string, ReturnType<typeof tool>> {
|
|
14
|
+
return {
|
|
15
|
+
memory_store: tool({
|
|
16
|
+
description:
|
|
17
|
+
"Store a memory. Returns the stored memory with its assigned id.",
|
|
18
|
+
args: {
|
|
19
|
+
content: tool.schema
|
|
20
|
+
.string()
|
|
21
|
+
.max(50000)
|
|
22
|
+
.describe("Memory content (required, max 50000 chars)"),
|
|
23
|
+
source: tool.schema
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Which agent wrote this memory"),
|
|
27
|
+
tags: tool.schema
|
|
28
|
+
.array(tool.schema.string())
|
|
29
|
+
.max(20)
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Filterable tags (max 20)"),
|
|
32
|
+
metadata: tool.schema
|
|
33
|
+
.record(tool.schema.string(), tool.schema.unknown())
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Arbitrary structured data"),
|
|
36
|
+
},
|
|
37
|
+
async execute(args) {
|
|
38
|
+
try {
|
|
39
|
+
const input: CreateMemoryInput = {
|
|
40
|
+
content: args.content,
|
|
41
|
+
source: args.source,
|
|
42
|
+
tags: args.tags,
|
|
43
|
+
metadata: args.metadata as Record<string, unknown> | undefined,
|
|
44
|
+
};
|
|
45
|
+
const result = await backend.store(input);
|
|
46
|
+
return JSON.stringify({ ok: true, data: result });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return JSON.stringify({
|
|
49
|
+
ok: false,
|
|
50
|
+
error: err instanceof Error ? err.message : String(err),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
memory_search: tool({
|
|
57
|
+
description:
|
|
58
|
+
"Search memories using hybrid vector + keyword search. Higher score = more relevant.",
|
|
59
|
+
args: {
|
|
60
|
+
q: tool.schema.string().optional().describe("Search query"),
|
|
61
|
+
tags: tool.schema
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("Comma-separated tags to filter by (AND)"),
|
|
65
|
+
source: tool.schema
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Filter by source agent"),
|
|
69
|
+
limit: tool.schema
|
|
70
|
+
.number()
|
|
71
|
+
.int()
|
|
72
|
+
.min(1)
|
|
73
|
+
.max(200)
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Max results (default 20, max 200)"),
|
|
76
|
+
offset: tool.schema
|
|
77
|
+
.number()
|
|
78
|
+
.int()
|
|
79
|
+
.min(0)
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("Pagination offset"),
|
|
82
|
+
memory_type: tool.schema
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("Comma-separated memory types to filter by (e.g. insight,pinned)"),
|
|
86
|
+
},
|
|
87
|
+
async execute(args) {
|
|
88
|
+
try {
|
|
89
|
+
const input: SearchInput = {
|
|
90
|
+
q: args.q,
|
|
91
|
+
tags: args.tags,
|
|
92
|
+
source: args.source,
|
|
93
|
+
limit: args.limit,
|
|
94
|
+
offset: args.offset,
|
|
95
|
+
memory_type: args.memory_type,
|
|
96
|
+
};
|
|
97
|
+
const result = await backend.search(input);
|
|
98
|
+
return JSON.stringify({ ok: true, ...result });
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return JSON.stringify({
|
|
101
|
+
ok: false,
|
|
102
|
+
error: err instanceof Error ? err.message : String(err),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
|
|
108
|
+
memory_get: tool({
|
|
109
|
+
description: "Retrieve a single memory by its id.",
|
|
110
|
+
args: {
|
|
111
|
+
id: tool.schema.string().describe("Memory id (UUID)"),
|
|
112
|
+
},
|
|
113
|
+
async execute(args) {
|
|
114
|
+
try {
|
|
115
|
+
const result = await backend.get(args.id);
|
|
116
|
+
if (!result) {
|
|
117
|
+
return JSON.stringify({ ok: false, error: "memory not found" });
|
|
118
|
+
}
|
|
119
|
+
return JSON.stringify({ ok: true, data: result });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return JSON.stringify({
|
|
122
|
+
ok: false,
|
|
123
|
+
error: err instanceof Error ? err.message : String(err),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
|
|
129
|
+
memory_update: tool({
|
|
130
|
+
description:
|
|
131
|
+
"Update an existing memory. Only provided fields are changed.",
|
|
132
|
+
args: {
|
|
133
|
+
id: tool.schema.string().describe("Memory id to update"),
|
|
134
|
+
content: tool.schema.string().optional().describe("New content"),
|
|
135
|
+
source: tool.schema.string().optional().describe("New source"),
|
|
136
|
+
tags: tool.schema
|
|
137
|
+
.array(tool.schema.string())
|
|
138
|
+
.optional()
|
|
139
|
+
.describe("Replacement tags"),
|
|
140
|
+
metadata: tool.schema
|
|
141
|
+
.record(tool.schema.string(), tool.schema.unknown())
|
|
142
|
+
.optional()
|
|
143
|
+
.describe("Replacement metadata"),
|
|
144
|
+
},
|
|
145
|
+
async execute(args) {
|
|
146
|
+
try {
|
|
147
|
+
const { id, ...rest } = args;
|
|
148
|
+
const input: UpdateMemoryInput = {
|
|
149
|
+
content: rest.content,
|
|
150
|
+
source: rest.source,
|
|
151
|
+
tags: rest.tags,
|
|
152
|
+
metadata: rest.metadata as Record<string, unknown> | undefined,
|
|
153
|
+
};
|
|
154
|
+
const result = await backend.update(id, input);
|
|
155
|
+
if (!result) {
|
|
156
|
+
return JSON.stringify({ ok: false, error: "memory not found" });
|
|
157
|
+
}
|
|
158
|
+
return JSON.stringify({ ok: true, data: result });
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return JSON.stringify({
|
|
161
|
+
ok: false,
|
|
162
|
+
error: err instanceof Error ? err.message : String(err),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
|
|
168
|
+
memory_delete: tool({
|
|
169
|
+
description: "Delete a memory by id.",
|
|
170
|
+
args: {
|
|
171
|
+
id: tool.schema.string().describe("Memory id to delete"),
|
|
172
|
+
},
|
|
173
|
+
async execute(args) {
|
|
174
|
+
try {
|
|
175
|
+
const deleted = await backend.remove(args.id);
|
|
176
|
+
if (!deleted) {
|
|
177
|
+
return JSON.stringify({ ok: false, error: "memory not found" });
|
|
178
|
+
}
|
|
179
|
+
return JSON.stringify({ ok: true });
|
|
180
|
+
} catch (err) {
|
|
181
|
+
return JSON.stringify({
|
|
182
|
+
ok: false,
|
|
183
|
+
error: err instanceof Error ? err.message : String(err),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
}),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Mem9CredentialsFile, Mem9Profile } from "./types.js";
|
|
2
|
+
|
|
3
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isMem9Profile(value: unknown): value is Mem9Profile {
|
|
8
|
+
if (!isRecord(value)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
typeof value.label === "string" &&
|
|
14
|
+
typeof value.baseUrl === "string" &&
|
|
15
|
+
typeof value.apiKey === "string"
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeMem9Profile(value: Mem9Profile): Mem9Profile {
|
|
20
|
+
return {
|
|
21
|
+
label: value.label,
|
|
22
|
+
baseUrl: value.baseUrl,
|
|
23
|
+
apiKey: value.apiKey,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseCredentialsFile(raw: string): Mem9CredentialsFile {
|
|
28
|
+
let parsed: unknown;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(raw) as unknown;
|
|
31
|
+
} catch {
|
|
32
|
+
throw new Error("invalid mem9 credentials file");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!isRecord(parsed) || parsed.schemaVersion !== 1 || !isRecord(parsed.profiles)) {
|
|
36
|
+
throw new Error("invalid mem9 credentials file");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const profiles: Record<string, Mem9Profile> = {};
|
|
40
|
+
for (const [profileID, profile] of Object.entries(parsed.profiles)) {
|
|
41
|
+
if (!isMem9Profile(profile)) {
|
|
42
|
+
throw new Error("invalid mem9 credentials file");
|
|
43
|
+
}
|
|
44
|
+
profiles[profileID] = normalizeMem9Profile(profile);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
schemaVersion: 1,
|
|
49
|
+
profiles,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function stringifyCredentialsFile(file: Mem9CredentialsFile): string {
|
|
54
|
+
return JSON.stringify(file, null, 2) + "\n";
|
|
55
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Mem9ConfigFile } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_SCOPE_CONFIG: Required<
|
|
4
|
+
Pick<
|
|
5
|
+
Mem9ConfigFile,
|
|
6
|
+
"schemaVersion" | "debug" | "defaultTimeoutMs" | "searchTimeoutMs"
|
|
7
|
+
>
|
|
8
|
+
> = {
|
|
9
|
+
schemaVersion: 1,
|
|
10
|
+
debug: false,
|
|
11
|
+
defaultTimeoutMs: 8000,
|
|
12
|
+
searchTimeoutMs: 15000,
|
|
13
|
+
};
|