loom-claw 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.
@@ -0,0 +1,38 @@
1
+ {
2
+ "id": "loom-claw",
3
+ "kind": "context-engine",
4
+ "uiHints": {
5
+ "loomBaseUrl": {
6
+ "label": "Loom Backend URL",
7
+ "help": "URL of the running Loom FastAPI server (default: http://localhost:8666)"
8
+ },
9
+ "sessionId": {
10
+ "label": "Loom Session ID",
11
+ "help": "Session ID to use on the Loom backend (default: 'default')"
12
+ },
13
+ "schemaId": {
14
+ "label": "Schema ID",
15
+ "help": "Shared schema file ID for cross-session memory (default: 'default')"
16
+ },
17
+ "schemaTemplate": {
18
+ "label": "Schema Template",
19
+ "help": "Initial schema template to apply: 'user_profile', 'general', or 'roleplay'"
20
+ },
21
+ "buildEveryNTurns": {
22
+ "label": "Build Every N Turns",
23
+ "help": "Run schema extraction every N conversation turns (default: 1)"
24
+ }
25
+ },
26
+ "configSchema": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "properties": {
30
+ "enabled": { "type": "boolean" },
31
+ "loomBaseUrl": { "type": "string" },
32
+ "sessionId": { "type": "string" },
33
+ "schemaId": { "type": "string" },
34
+ "schemaTemplate": { "type": "string" },
35
+ "buildEveryNTurns": { "type": "integer", "minimum": 1 }
36
+ }
37
+ }
38
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "loom-claw",
3
+ "version": "0.1.0",
4
+ "description": "Loom cognitive memory system as an OpenClaw Context Engine plugin — connects to a Loom Python backend for structured schema-based long-term memory",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "openclaw",
10
+ "openclaw-plugin",
11
+ "context-engine",
12
+ "loom",
13
+ "cognitive-memory",
14
+ "long-term-memory",
15
+ "schema-memory",
16
+ "llm"
17
+ ],
18
+ "files": [
19
+ "index.ts",
20
+ "src/**/*.ts",
21
+ "openclaw.plugin.json",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "scripts": {
26
+ "test": "vitest run --dir test",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "dependencies": {
30
+ "@sinclair/typebox": "^0.34.0"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.7.0",
34
+ "vitest": "^3.0.0"
35
+ },
36
+ "peerDependencies": {
37
+ "openclaw": "*"
38
+ },
39
+ "openclaw": {
40
+ "extensions": [
41
+ "./index.ts"
42
+ ],
43
+ "compat": {
44
+ "pluginApi": ">=2026.4.7",
45
+ "minGatewayVersion": "2026.4.7"
46
+ },
47
+ "build": {
48
+ "openclawVersion": "2026.4.7",
49
+ "pluginSdkVersion": "2026.4.7"
50
+ }
51
+ }
52
+ }
package/src/client.ts ADDED
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Loom API client — thin HTTP wrapper around the Loom FastAPI backend.
3
+ *
4
+ * All core cognitive memory logic stays in the Python backend.
5
+ * This client simply calls the right endpoints and returns typed results.
6
+ */
7
+
8
+ import type {
9
+ BuildResponse,
10
+ RecallAllResponse,
11
+ RecallResponse,
12
+ InspectAllResponse,
13
+ SchemaInfo,
14
+ SchemaFileDataResponse,
15
+ } from "./types.js";
16
+
17
+ export class LoomClient {
18
+ private baseUrl: string;
19
+
20
+ constructor(baseUrl: string) {
21
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
22
+ }
23
+
24
+ async build(
25
+ text: string,
26
+ sessionId: string,
27
+ sourceSessionId = "",
28
+ ): Promise<BuildResponse> {
29
+ const body: Record<string, string> = { text, session_id: sessionId };
30
+ if (sourceSessionId) body.source_session_id = sourceSessionId;
31
+ return this._post<BuildResponse>("/api/build", body);
32
+ }
33
+
34
+ async recallAll(schemaId: string): Promise<RecallAllResponse> {
35
+ const params = schemaId ? `?schema_id=${encodeURIComponent(schemaId)}` : "";
36
+ return this._get<RecallAllResponse>(`/api/schemas/recall-all${params}`);
37
+ }
38
+
39
+ async recall(message: string, sessionId: string, schemaId?: string): Promise<RecallResponse> {
40
+ return this._post<RecallResponse>("/api/schemas/recall", {
41
+ message,
42
+ session_id: sessionId,
43
+ schema_id: schemaId || "",
44
+ });
45
+ }
46
+
47
+ async inspectAll(
48
+ schemaId: string,
49
+ showValues = true,
50
+ maxDepth = -1,
51
+ ): Promise<InspectAllResponse> {
52
+ const qs = new URLSearchParams();
53
+ if (schemaId) qs.set("schema_id", schemaId);
54
+ qs.set("show_values", String(showValues));
55
+ qs.set("max_depth", String(maxDepth));
56
+ return this._get<InspectAllResponse>(`/api/schemas/inspect-all?${qs}`);
57
+ }
58
+
59
+ async listSchemas(): Promise<string[]> {
60
+ return this._get<string[]>("/api/schemas");
61
+ }
62
+
63
+ async getSchema(name: string): Promise<SchemaInfo> {
64
+ return this._get<SchemaInfo>(`/api/schemas/${encodeURIComponent(name)}`);
65
+ }
66
+
67
+ async deleteSchema(name: string, schemaId = ""): Promise<{ status: string; message: string }> {
68
+ const qs = schemaId ? `?schema_id=${encodeURIComponent(schemaId)}` : "";
69
+ return this._delete(`/api/schemas/${encodeURIComponent(name)}${qs}`);
70
+ }
71
+
72
+ async clearAllSchemas(schemaId = ""): Promise<{ status: string; message: string; count: number }> {
73
+ const qs = schemaId ? `?schema_id=${encodeURIComponent(schemaId)}` : "";
74
+ return this._delete(`/api/schemas${qs}`);
75
+ }
76
+
77
+ async createSchema(
78
+ name: string,
79
+ fields?: Record<string, { value: string; description: string }>,
80
+ ): Promise<SchemaInfo> {
81
+ return this._post<SchemaInfo>("/api/schemas", { name, fields });
82
+ }
83
+
84
+ async getSchemaFileData(schemaId: string): Promise<SchemaFileDataResponse> {
85
+ return this._get<SchemaFileDataResponse>(
86
+ `/api/schemas/file-data/${encodeURIComponent(schemaId)}`,
87
+ );
88
+ }
89
+
90
+ async createSchemaFromTemplate(
91
+ templateName: string,
92
+ customFields?: Record<string, { value: string; description: string }>,
93
+ ): Promise<SchemaInfo> {
94
+ return this._post<SchemaInfo>("/api/schemas/from-template", {
95
+ template_name: templateName,
96
+ custom_fields: customFields,
97
+ });
98
+ }
99
+
100
+ async listTemplates(): Promise<Array<{ name: string; description: string }>> {
101
+ return this._get("/api/templates");
102
+ }
103
+
104
+ async listTemplatesGrouped(): Promise<Record<string, Array<{ name: string; description: string; source: string; domains: string[]; domain_count: number }>>> {
105
+ return this._get("/api/templates/grouped");
106
+ }
107
+
108
+ async getTemplate(name: string): Promise<{
109
+ name: string;
110
+ description: string;
111
+ source: string;
112
+ domains: string[];
113
+ data: Record<string, unknown>;
114
+ }> {
115
+ return this._get(`/api/templates/${encodeURIComponent(name)}`);
116
+ }
117
+
118
+ async saveCustomTemplate(data: Record<string, unknown>): Promise<{ status: string; path: string; name: string }> {
119
+ return this._post("/api/templates/custom", data);
120
+ }
121
+
122
+ async deleteCustomTemplate(name: string): Promise<{ status: string; name: string }> {
123
+ return this._delete(`/api/templates/custom/${encodeURIComponent(name)}`);
124
+ }
125
+
126
+ async createSession(
127
+ sessionId: string,
128
+ schemaId = "default",
129
+ sourceSessionId = "",
130
+ ): Promise<{ status: string; session_id: string; schema_id: string }> {
131
+ const qs = new URLSearchParams({ session_id: sessionId, schema_id: schemaId });
132
+ if (sourceSessionId) qs.set("source_session_id", sourceSessionId);
133
+ return this._post(`/api/sessions?${qs}`, undefined);
134
+ }
135
+
136
+ async switchSchema(
137
+ sessionId: string,
138
+ targetSchemaId: string,
139
+ sourceSessionId = "",
140
+ ): Promise<{ status: string; session_id: string; schema_id: string; message?: string }> {
141
+ const body: Record<string, string> = {
142
+ session_id: sessionId,
143
+ target_schema_id: targetSchemaId,
144
+ };
145
+ if (sourceSessionId) body.source_session_id = sourceSessionId;
146
+ return this._post("/api/schemas/switch", body);
147
+ }
148
+
149
+ async listSessions(): Promise<Array<{ session_id: string; schema_id: string; file_size: number; chat_rounds: number; schema_count: number }>> {
150
+ return this._get("/api/sessions");
151
+ }
152
+
153
+ async updateSchemaFromChat(
154
+ sessionId: string,
155
+ rounds?: number,
156
+ ): Promise<{ status: string; message: string }> {
157
+ return this._post("/api/update-schema-from-chat", {
158
+ session_id: sessionId,
159
+ rounds,
160
+ });
161
+ }
162
+
163
+ async newSchema(sessionId: string): Promise<{ status: string; backup_id?: string }> {
164
+ return this._post("/api/schemas/new", { session_id: sessionId });
165
+ }
166
+
167
+ async createSchemaFile(
168
+ sessionId: string,
169
+ schemaId = "",
170
+ template = "",
171
+ ): Promise<{ status: string; schema_id: string; template?: string }> {
172
+ return this._post("/api/schemas/create-file", {
173
+ schema_id: schemaId,
174
+ session_id: sessionId,
175
+ template,
176
+ });
177
+ }
178
+
179
+ async listSavedSchemas(): Promise<Array<{ schema_id: string; file_size: number; domain_count: number }>> {
180
+ return this._get("/api/schemas/saved");
181
+ }
182
+
183
+ async listBackups(schemaId = ""): Promise<Array<{ backup_id: string; file_size: number; created_at: string }>> {
184
+ const qs = schemaId ? `?schema_id=${encodeURIComponent(schemaId)}` : "";
185
+ return this._get(`/api/schemas/backups${qs}`);
186
+ }
187
+
188
+ async restoreSchema(backupId: string, targetSchemaId = ""): Promise<{ status: string; backup_id: string }> {
189
+ return this._post("/api/schemas/restore", {
190
+ backup_id: backupId,
191
+ target_schema_id: targetSchemaId,
192
+ });
193
+ }
194
+
195
+ async ping(): Promise<boolean> {
196
+ try {
197
+ const resp = await fetch(`${this.baseUrl}/api/schemas`, {
198
+ method: "GET",
199
+ signal: AbortSignal.timeout(5000),
200
+ });
201
+ return resp.ok;
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
206
+
207
+ // -- Internal helpers --
208
+
209
+ private async _get<T>(path: string): Promise<T> {
210
+ const resp = await fetch(`${this.baseUrl}${path}`, {
211
+ method: "GET",
212
+ headers: { "Content-Type": "application/json" },
213
+ });
214
+ if (!resp.ok) {
215
+ const detail = await resp.text().catch(() => resp.statusText);
216
+ throw new Error(`Loom API error ${resp.status} on GET ${path}: ${detail}`);
217
+ }
218
+ return (await resp.json()) as T;
219
+ }
220
+
221
+ private async _post<T>(path: string, body: unknown): Promise<T> {
222
+ const resp = await fetch(`${this.baseUrl}${path}`, {
223
+ method: "POST",
224
+ headers: { "Content-Type": "application/json" },
225
+ body: body !== undefined ? JSON.stringify(body) : undefined,
226
+ });
227
+ if (!resp.ok) {
228
+ const detail = await resp.text().catch(() => resp.statusText);
229
+ throw new Error(`Loom API error ${resp.status} on POST ${path}: ${detail}`);
230
+ }
231
+ return (await resp.json()) as T;
232
+ }
233
+
234
+ private async _delete<T = Record<string, unknown>>(path: string): Promise<T> {
235
+ const resp = await fetch(`${this.baseUrl}${path}`, {
236
+ method: "DELETE",
237
+ headers: { "Content-Type": "application/json" },
238
+ });
239
+ if (!resp.ok) {
240
+ const detail = await resp.text().catch(() => resp.statusText);
241
+ throw new Error(`Loom API error ${resp.status} on DELETE ${path}: ${detail}`);
242
+ }
243
+ return (await resp.json()) as T;
244
+ }
245
+ }
package/src/config.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Plugin configuration resolution.
3
+ * Merges environment variables with plugin config values.
4
+ */
5
+
6
+ import type { LoomPluginConfig } from "./types.js";
7
+
8
+ export function resolveConfig(
9
+ env: NodeJS.ProcessEnv,
10
+ raw: Record<string, unknown>,
11
+ ): LoomPluginConfig {
12
+ return {
13
+ enabled: bool(raw.enabled, env.LOOM_ENABLED, true),
14
+ loomBaseUrl: str(
15
+ raw.loomBaseUrl,
16
+ env.LOOM_BASE_URL,
17
+ "http://localhost:8666",
18
+ ),
19
+ sessionId: str(raw.sessionId, env.LOOM_SESSION_ID, "default"),
20
+ schemaId: str(raw.schemaId, env.LOOM_SCHEMA_ID, "default"),
21
+ schemaTemplate: str(raw.schemaTemplate, env.LOOM_SCHEMA_TEMPLATE, ""),
22
+ buildEveryNTurns: num(raw.buildEveryNTurns, env.LOOM_BUILD_EVERY_N_TURNS, 1),
23
+ };
24
+ }
25
+
26
+ function str(...sources: Array<unknown>): string {
27
+ for (const s of sources) {
28
+ if (typeof s === "string" && s.trim()) return s.trim();
29
+ }
30
+ return "";
31
+ }
32
+
33
+ function num(...sources: Array<unknown>): number {
34
+ for (const s of sources) {
35
+ if (s !== undefined && s !== null && s !== "") {
36
+ const n = Number(s);
37
+ if (!Number.isNaN(n)) return Math.round(n);
38
+ }
39
+ }
40
+ return 0;
41
+ }
42
+
43
+ function bool(...sources: Array<unknown>): boolean {
44
+ for (const s of sources) {
45
+ if (typeof s === "boolean") return s;
46
+ if (typeof s === "string") {
47
+ const lower = s.toLowerCase();
48
+ if (lower === "true" || lower === "1" || lower === "yes") return true;
49
+ if (lower === "false" || lower === "0" || lower === "no") return false;
50
+ }
51
+ }
52
+ return true;
53
+ }