@totalreclaw/totalreclaw 3.3.12-rc.2 → 3.3.12-rc.21

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 (87) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/SKILL.md +34 -249
  3. package/api-client.ts +17 -9
  4. package/batch-gate.ts +42 -0
  5. package/billing-cache.ts +25 -20
  6. package/claims-helper.ts +7 -1
  7. package/config.ts +54 -12
  8. package/consolidation.ts +6 -13
  9. package/contradiction-sync.ts +19 -14
  10. package/credential-provider.ts +184 -0
  11. package/crypto.ts +27 -160
  12. package/dist/api-client.js +17 -9
  13. package/dist/batch-gate.js +40 -0
  14. package/dist/billing-cache.js +19 -21
  15. package/dist/claims-helper.js +7 -1
  16. package/dist/config.js +54 -12
  17. package/dist/consolidation.js +6 -15
  18. package/dist/contradiction-sync.js +15 -15
  19. package/dist/credential-provider.js +145 -0
  20. package/dist/crypto.js +17 -137
  21. package/dist/download-ux.js +11 -7
  22. package/dist/embedder-loader.js +266 -0
  23. package/dist/embedding.js +36 -3
  24. package/dist/entry.js +123 -0
  25. package/dist/extractor.js +134 -0
  26. package/dist/fs-helpers.js +116 -241
  27. package/dist/import-adapters/chatgpt-adapter.js +14 -0
  28. package/dist/import-adapters/claude-adapter.js +14 -0
  29. package/dist/import-adapters/gemini-adapter.js +43 -159
  30. package/dist/import-adapters/mcp-memory-adapter.js +14 -0
  31. package/dist/import-state-manager.js +100 -0
  32. package/dist/index.js +1130 -2520
  33. package/dist/llm-client.js +69 -1
  34. package/dist/memory-runtime.js +459 -0
  35. package/dist/native-memory.js +123 -0
  36. package/dist/onboarding-cli.js +3 -2
  37. package/dist/pair-cli.js +1 -1
  38. package/dist/pair-crypto.js +16 -358
  39. package/dist/pair-http.js +147 -4
  40. package/dist/relay.js +140 -0
  41. package/dist/reranker.js +13 -8
  42. package/dist/semantic-dedup.js +5 -7
  43. package/dist/skill-register.js +97 -0
  44. package/dist/subgraph-search.js +3 -1
  45. package/dist/subgraph-store.js +315 -282
  46. package/dist/tool-gating.js +39 -26
  47. package/dist/tools.js +367 -0
  48. package/dist/tr-cli-export-helper.js +103 -0
  49. package/dist/tr-cli.js +220 -127
  50. package/dist/trajectory-poller.js +155 -9
  51. package/dist/vault-crypto.js +551 -0
  52. package/download-ux.ts +12 -6
  53. package/embedder-loader.ts +293 -1
  54. package/embedding.ts +43 -3
  55. package/entry.ts +132 -0
  56. package/extractor.ts +167 -0
  57. package/fs-helpers.ts +166 -292
  58. package/import-adapters/chatgpt-adapter.ts +18 -0
  59. package/import-adapters/claude-adapter.ts +18 -0
  60. package/import-adapters/gemini-adapter.ts +56 -183
  61. package/import-adapters/mcp-memory-adapter.ts +18 -0
  62. package/import-adapters/types.ts +1 -1
  63. package/import-state-manager.ts +139 -0
  64. package/index.ts +1432 -3002
  65. package/llm-client.ts +74 -1
  66. package/memory-runtime.ts +723 -0
  67. package/native-memory.ts +196 -0
  68. package/onboarding-cli.ts +3 -2
  69. package/openclaw.plugin.json +5 -17
  70. package/package.json +7 -4
  71. package/pair-cli.ts +1 -1
  72. package/pair-crypto.ts +41 -483
  73. package/pair-http.ts +194 -5
  74. package/postinstall.mjs +138 -0
  75. package/relay.ts +172 -0
  76. package/reranker.ts +13 -8
  77. package/semantic-dedup.ts +5 -6
  78. package/skill-register.ts +146 -0
  79. package/skill.json +1 -1
  80. package/subgraph-search.ts +3 -1
  81. package/subgraph-store.ts +334 -299
  82. package/tool-gating.ts +39 -26
  83. package/tools.ts +499 -0
  84. package/tr-cli-export-helper.ts +138 -0
  85. package/tr-cli.ts +263 -133
  86. package/trajectory-poller.ts +162 -10
  87. package/vault-crypto.ts +705 -0
package/tool-gating.ts CHANGED
@@ -1,12 +1,30 @@
1
1
  /**
2
- * Tool gating predicate for 3.2.0 — the `before_tool_call` hook in index.ts
3
- * delegates to this module so the logic is testable without standing up a
4
- * full OpenClaw plugin host.
2
+ * Tool gating predicate — the `before_tool_call` hook in index.ts delegates
3
+ * to this module so the logic is testable without standing up a full
4
+ * OpenClaw plugin host.
5
5
  *
6
- * Scope: the 3.2.0 state machine has two states (`fresh`, `active`). Memory
7
- * tools are blocked when state is anything other than `active`. Billing +
8
- * setup-adjacent tools remain usable users need to be able to upgrade,
9
- * migrate, and start onboarding before their vault is active.
6
+ * Scope (Phase 3.3 OpenClaw native integration): the agent-facing memory
7
+ * tools are now the bundled NATIVE tools `memory_search` / `memory_get`
8
+ * (registered via the MemoryPluginCapability in index.ts). The legacy
9
+ * `totalreclaw_*` agent tools were retired in Task 3.2. This gate now
10
+ * blocks the NATIVE memory tools until onboarding state is `active`, so
11
+ * that an unpaired agent receives an actionable non-secret pointer to the
12
+ * CLI pair surface instead of silently seeing "no memories found" from the
13
+ * adapter's fail-soft empty-result path.
14
+ *
15
+ * Why gate the natives rather than rely on adapter fail-soft:
16
+ * - The adapter's `recall()` closure returns `[]` when `needsSetup`, which
17
+ * surfaces to the agent as "no memories matched" — indistinguishable
18
+ * from an empty vault. The gate intercepts BEFORE the tool runs and
19
+ * returns a blockReason telling the agent exactly how to onboard
20
+ * (`tr pair --url-pin`). Without the gate, a fresh user asking the
21
+ * agent "what do you remember about me?" gets a confident "nothing"
22
+ * with no path forward.
23
+ *
24
+ * State machine: `fresh` → `active`. Memory tools are blocked unless state
25
+ * is `active`. The pair surface itself (`tr pair`, the `/pair/start` HTTP
26
+ * route) is NOT gated — users must be able to start onboarding before
27
+ * their vault is active.
10
28
  *
11
29
  * This module imports ONLY types + the state resolver. No I/O beyond what
12
30
  * `resolveOnboardingState` already does; no network; no env reads.
@@ -15,24 +33,17 @@
15
33
  import type { OnboardingState } from './fs-helpers.js';
16
34
 
17
35
  /**
18
- * Tool names gated on `state=active`. Keep in sync with the actual
19
- * `registerTool` calls in `index.ts`. Anything NOT in this set is always
20
- * callable (e.g. totalreclaw_upgrade, totalreclaw_migrate,
21
- * totalreclaw_onboarding_start, totalreclaw_setup).
36
+ * Tool names gated on `state=active`. These are the bundled NATIVE memory
37
+ * tools the agent sees after the Phase 2 MemoryPluginCapability registration.
38
+ * Anything NOT in this set is always callable.
39
+ *
40
+ * Keep in sync with the `registerNativeMemory` registration in `index.ts`
41
+ * (the two `api.registerTool` calls with `names: ['memory_search']` and
42
+ * `names: ['memory_get']`).
22
43
  */
23
44
  export const GATED_TOOL_NAMES: readonly string[] = Object.freeze([
24
- 'totalreclaw_remember',
25
- 'totalreclaw_recall',
26
- 'totalreclaw_forget',
27
- 'totalreclaw_export',
28
- 'totalreclaw_status',
29
- 'totalreclaw_consolidate',
30
- 'totalreclaw_pin',
31
- 'totalreclaw_unpin',
32
- 'totalreclaw_retype',
33
- 'totalreclaw_set_scope',
34
- 'totalreclaw_import_from',
35
- 'totalreclaw_import_batch',
45
+ 'memory_search',
46
+ 'memory_get',
36
47
  ]);
37
48
 
38
49
  export interface GateDecision {
@@ -57,9 +68,11 @@ export function decideToolGate(
57
68
  return {
58
69
  block: true,
59
70
  blockReason:
60
- 'TotalReclaw onboarding required. Run `openclaw totalreclaw onboard` ' +
61
- 'in a terminal (or call the `totalreclaw_onboarding_start` tool for ' +
62
- 'details). Memory tools are gated until the user completes setup.',
71
+ 'TotalReclaw setup pending memory_search/memory_get are disabled until the user pairs. ' +
72
+ 'Tell the user to run `tr pair --url-pin` on the gateway host and open the returned URL ' +
73
+ 'in a browser (the recovery phrase is generated and encrypted in-browser; it never enters ' +
74
+ 'this chat). Once paired, memory tools unlock automatically. Do NOT attempt to generate, ' +
75
+ 'display, or relay a recovery phrase yourself.',
63
76
  };
64
77
  }
65
78
 
package/tools.ts ADDED
@@ -0,0 +1,499 @@
1
+ /**
2
+ * tools — the agent-facing memory_search / memory_get tool factories.
3
+ *
4
+ * Task 2.6 of the OpenClaw native integration plan
5
+ * (docs/plans/2026-06-21-openclaw-native-integration-plan.md, 2026-06-21).
6
+ *
7
+ * WHY THIS FILE EXISTS:
8
+ * OpenClaw's `active-memory` sub-agent and the host's memory subsystem
9
+ * call two tools by name: `memory_search` and `memory_get`. The bundled
10
+ * memory-core registers them via
11
+ * api.registerTool((ctx) => createLazyMemorySearchTool(
12
+ * resolveMemoryToolOptions(ctx)), { names: ["memory_search"] })
13
+ * and the matching Get call. For TR to be the memory backend, TR must
14
+ * register tools with the SAME names + parameter schema + return shape
15
+ * that route into TR's encrypted-vault pipeline instead of memory-core's
16
+ * file backend.
17
+ *
18
+ * These factories produce those tools. They delegate to the TR
19
+ * MemorySearchManager obtained from the TR MemoryPluginRuntime (Task 2.1 /
20
+ * 2.3 — `createTrMemoryPluginRuntime`), which synthesizes file-shaped
21
+ * results from decrypted facts so the agent sees a familiar memory corpus.
22
+ *
23
+ * MEMORY-CORE'S PATTERN (what this mirrors):
24
+ * Reverse-engineered from OpenClaw 2026.6.8's bundled memory-core at
25
+ * /tmp/tr-openclaw-probe/node_modules/openclaw/dist/tools-CT_OGlM3.js
26
+ * (createMemorySearchTool l.437, createMemoryGetTool l.631)
27
+ * and the memory-core registration at
28
+ * dist/extensions/memory-core/index.js (l.273-274).
29
+ *
30
+ * Tool name + label + description: cribbed verbatim from memory-core so
31
+ * the active-memory sub-agent treats TR's tools as drop-in.
32
+ * Parameter schema: field-for-field match with memory-core's
33
+ * MemorySearchSchema = { query, maxResults?, minScore?, corpus? }
34
+ * MemoryGetSchema = { path, from?, lines?, corpus? }
35
+ * Return shape:
36
+ * search -> { results: MemorySearchResult[], provider?, mode?, debug? }
37
+ * get -> { path, text, truncated?, from?, lines?, nextFrom? }
38
+ * unavailable -> { disabled:true, unavailable:true, error, warning, action }
39
+ * (mirrors memory-core's buildMemorySearchUnavailableResult so the
40
+ * agent surfaces the same warning/action guidance to the user).
41
+ *
42
+ * CAPTURED-RUNTIME DESIGN (vs ctx-resolved):
43
+ * memory-core resolves the manager from `ctx` via an internal helper
44
+ * `getMemoryManagerContextWithPurpose({cfg, agentId})` which calls
45
+ * `loadMemoryToolRuntime()` — a private import from a hashed dist chunk
46
+ * (`./tools.runtime.js`) that only bundled plugins can reach. A
47
+ * third-party plugin (TR) cannot import that chunk and cannot reach the
48
+ * memory runtime via `ctx`.
49
+ *
50
+ * TR solves this by capturing the runtime at register() time. Task 2.7's
51
+ * register() creates the TR MemoryPluginRuntime via
52
+ * `createTrMemoryPluginRuntime(deps)` and hands it to BOTH:
53
+ * - api.registerMemoryCapability({ runtime, ... }) (the host surface)
54
+ * - createMemorySearchTool(runtime) / createMemoryGetTool(runtime)
55
+ * (the tool surface, captured here)
56
+ * so the tool handler calls `runtime.getMemorySearchManager(...)` to get
57
+ * the manager — the SAME surface the host calls. This is the sanctioned
58
+ * deviation; it is documented here so Task 2.7 knows the wiring.
59
+ *
60
+ * What TR does NOT mirror from memory-core (and why):
61
+ * - corpus=wiki / corpus=all supplements: memory-core-only feature
62
+ * (registered compiled-wiki supplements). TR has no supplements; the
63
+ * param is accepted for schema parity but treated as memory-only.
64
+ * - qmd backend / dreaming / citations-mode: memory-core-only. TR's
65
+ * adapter is its own backend.
66
+ * - cooldown tracking on unavailable: memory-core caches "unavailable"
67
+ * for N seconds to avoid hammering a broken embedder. TR's adapter
68
+ * reaches the manager via a cheap closure (no embedder spin-up on
69
+ * each call), so the cooldown is not needed at this layer; the
70
+ * underlying recall pipeline already debounces.
71
+ *
72
+ * SCANNER-CLEAN HARD CONTRACT (env=N net=N):
73
+ * This file is pure orchestration. It touches NO host environment state
74
+ * and performs NO outbound network I/O. The manager arrives via the
75
+ * injected runtime; the tool handler only awaits its methods.
76
+ *
77
+ * The TR adapter's content-read method is `readFile` (its canonical name).
78
+ * It is called here by that literal name — the scanner's per-file exfil
79
+ * rule requires env+net token co-occurrence to flag, and this file has no
80
+ * network token, so the bare method name is clean. (Earlier revisions
81
+ * obscured the name behind a `readContent`/`readFile` indirection on a
82
+ * mistaken belief that the name alone was a trigger; that indirection was
83
+ * dead code and has been removed.)
84
+ *
85
+ * `npm run check-scanner` must remain 0 flags.
86
+ *
87
+ * `npm run build` uses `--noCheck`, so structural/loose typing where
88
+ * OpenClaw's types aren't importable is safe (the plugin does not depend
89
+ * on OpenClaw's type package).
90
+ */
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Types — loose on purpose. The plugin does not import OpenClaw's type
94
+ // package; the tool object returned is STRUCTURALLY compatible with
95
+ // OpenClaw's AnyAgentTool at runtime (same field names, same execute arity,
96
+ // same AgentToolResult shape). See dist/common-BYJ5YAFM.d.ts l.20 +
97
+ // dist/index-CB3EOAcX.d.ts l.406/422 for the canonical shapes.
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /**
101
+ * A search result hit surfaced to the agent. Mirrors memory-core's
102
+ * MemorySearchResult — the agent parses this shape out of the JSON payload.
103
+ * TR's TrMemorySearchManager already synthesizes hits in exactly this shape
104
+ * (Task 2.1), so the tool just forwards what the manager returns.
105
+ */
106
+ export interface MemorySearchResult {
107
+ path: string;
108
+ startLine: number;
109
+ endLine: number;
110
+ score: number;
111
+ snippet: string;
112
+ source: string;
113
+ citation?: string;
114
+ [k: string]: unknown;
115
+ }
116
+
117
+ /**
118
+ * A read result returned by memory_get. Mirrors memory-core's
119
+ * executeMemoryReadResult payload — { path, text, truncated?, from?, lines?,
120
+ * nextFrom? } on success; { path, text:"", disabled:true, error } on failure.
121
+ */
122
+ export interface MemoryGetResult {
123
+ path: string;
124
+ text: string;
125
+ truncated?: boolean;
126
+ from?: number;
127
+ lines?: number;
128
+ nextFrom?: number;
129
+ [k: string]: unknown;
130
+ }
131
+
132
+ /**
133
+ * The TR MemoryPluginRuntime surface these tools need. Exactly the shape
134
+ * Task 2.1 / 2.3's `createTrMemoryPluginRuntime` returns. Kept loose so
135
+ * tools.ts does not import memory-runtime.ts (avoids a cycle and keeps the
136
+ * tool factories independently testable).
137
+ */
138
+ export interface TrMemoryPluginRuntimeLike {
139
+ getMemorySearchManager(params: {
140
+ cfg?: unknown;
141
+ agentId?: string;
142
+ purpose?: string;
143
+ }): Promise<{
144
+ manager: TrMemorySearchManagerLike | null;
145
+ error?: string;
146
+ }>;
147
+ }
148
+
149
+ /**
150
+ * The subset of the TR MemorySearchManager these tools call. Methods the
151
+ * tools do not use (status / probes / close) are omitted so the loose type
152
+ * stays minimal. The content-read method is named the same as on the real
153
+ * adapter (`readFile`) — this file is pure orchestration with no network
154
+ * token, so the literal method name does not trip the scanner's per-file
155
+ * exfil rule (which requires env+net co-occurrence).
156
+ */
157
+ export interface TrMemorySearchManagerLike {
158
+ search(
159
+ query: string,
160
+ opts?: {
161
+ maxResults?: number;
162
+ minScore?: number;
163
+ signal?: AbortSignal;
164
+ sessionKey?: string;
165
+ },
166
+ ): Promise<MemorySearchResult[]>;
167
+ readFile(p: {
168
+ relPath: string;
169
+ from?: number;
170
+ lines?: number;
171
+ }): Promise<MemoryGetResult>;
172
+ }
173
+
174
+ /**
175
+ * An AgentToolResult. `{ content: [{type:'text', text: JSON.stringify(payload)}] }`
176
+ * — the agent parses the JSON in `text`. This matches memory-core's
177
+ * `jsonResult(payload)` helper (dist/common-BYJ5YAFM.d.ts l.103) and the
178
+ * shape the retired TR agent tools used to return (the convention is kept
179
+ * for the native memory_search/memory_get wrappers).
180
+ */
181
+ export interface AgentToolResultLike {
182
+ content: Array<{ type: 'text'; text: string }>;
183
+ }
184
+
185
+ /**
186
+ * The full tool object these factories return. Structurally compatible with
187
+ * OpenClaw's AnyAgentTool: name + description + parameters (the Tool base,
188
+ * dist/types-Boa_mcGH.d.ts l.216) + label + execute (AgentTool,
189
+ * dist/index-CB3EOAcX.d.ts l.422). Task 2.7 passes this to
190
+ * `api.registerTool(() => createMemorySearchTool(runtime), { names:[...] })`.
191
+ */
192
+ export interface AgentToolLike {
193
+ name: string;
194
+ label: string;
195
+ description: string;
196
+ parameters: object;
197
+ execute: (
198
+ toolCallId: string,
199
+ params: unknown,
200
+ signal?: AbortSignal,
201
+ onUpdate?: unknown,
202
+ ) => Promise<AgentToolResultLike>;
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Helpers
207
+ // ---------------------------------------------------------------------------
208
+
209
+ /**
210
+ * Build an AgentToolResult whose content text is the JSON-encoded payload.
211
+ * Mirrors memory-core's `jsonResult` helper exactly. The agent reads
212
+ * `content[0].text` and JSON-parses it.
213
+ */
214
+ function jsonResult(payload: unknown): AgentToolResultLike {
215
+ return {
216
+ content: [{ type: 'text', text: JSON.stringify(payload) }],
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Build the unavailable-result payload. Mirrors memory-core's
222
+ * buildMemorySearchUnavailableResult (dist/tools-CT_OGlM3.js l.219): the
223
+ * payload carries `disabled:true` + `unavailable:true` (the signals
224
+ * active-memory keys off to tell the user memory is down) PLUS `warning`
225
+ * and `action` guidance the agent forwards to the user.
226
+ *
227
+ * The warning/action wording is TR-adapted (TR has no embedding-quota state
228
+ * at this layer; the error string is whatever the runtime/pipeline surfaced).
229
+ */
230
+ function buildUnavailableResult(error: string | undefined): {
231
+ disabled: true;
232
+ unavailable: true;
233
+ error: string;
234
+ warning: string;
235
+ action: string;
236
+ } {
237
+ const reason = (error ?? 'memory search unavailable').trim() || 'memory search unavailable';
238
+ return {
239
+ disabled: true,
240
+ unavailable: true,
241
+ error: reason,
242
+ warning: 'Memory recall is unavailable. You should tell the user that prior memories cannot be retrieved right now.',
243
+ action: 'Let the user know memory recall is unavailable and suggest they retry or check their TotalReclaw pairing.',
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Read a string param from a loosely-typed args object. Mirrors memory-core's
249
+ * readStringParam contract (required + optional variants). Throws a plain
250
+ * Error on missing-required; memory-core throws ToolInputError but the tool
251
+ * handler catches and converts any thrown error into a disabled result, so
252
+ * the error class does not matter at this boundary.
253
+ */
254
+ function readStringParam(
255
+ params: Record<string, unknown>,
256
+ key: string,
257
+ opts: { required?: true } = {},
258
+ ): string | undefined {
259
+ const v = params[key];
260
+ if (typeof v === 'string') return v;
261
+ if (opts.required === true) {
262
+ throw new Error(`missing required string parameter: ${key}`);
263
+ }
264
+ return undefined;
265
+ }
266
+
267
+ /**
268
+ * Read a positive-integer param. Mirrors memory-core's
269
+ * readPositiveIntegerParam. Returns undefined when absent/invalid (the
270
+ * manager applies its own default).
271
+ */
272
+ function readPositiveIntegerParam(
273
+ params: Record<string, unknown>,
274
+ key: string,
275
+ ): number | undefined {
276
+ const v = params[key];
277
+ if (typeof v === 'number' && Number.isInteger(v) && v >= 1) return v;
278
+ return undefined;
279
+ }
280
+
281
+ /**
282
+ * Read a finite-number param. Mirrors memory-core's readFiniteNumberParam
283
+ * (used for minScore).
284
+ */
285
+ function readFiniteNumberParam(params: Record<string, unknown>, key: string): number | undefined {
286
+ const v = params[key];
287
+ if (typeof v === 'number' && Number.isFinite(v)) return v;
288
+ return undefined;
289
+ }
290
+
291
+ /**
292
+ * Coerce the unknown tool-params arg into a record. The agent emits a plain
293
+ * object; this is a defensive cast that also tolerates null/undefined.
294
+ */
295
+ function asParamsRecord(params: unknown): Record<string, unknown> {
296
+ return (params ?? {}) as Record<string, unknown>;
297
+ }
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // createMemorySearchTool
301
+ // ---------------------------------------------------------------------------
302
+
303
+ /**
304
+ * The memory_search tool factory. Returns a tool object structurally
305
+ * compatible with OpenClaw's AnyAgentTool, delegating to the TR
306
+ * MemorySearchManager obtained from the captured runtime.
307
+ *
308
+ * Wiring (Task 2.7):
309
+ * api.registerTool(() => createMemorySearchTool(runtime), { names: ["memory_search"] });
310
+ *
311
+ * Handler contract:
312
+ * - Resolves the manager via `runtime.getMemorySearchManager({purpose:"search"})`.
313
+ * - On { manager: null, error }: returns an unavailable result (disabled:true).
314
+ * - On a thrown manager.search error: returns an unavailable result.
315
+ * - On success: returns the manager's results wrapped in an AgentToolResult
316
+ * whose text payload is `{ results, provider, mode }` (memory-core shape).
317
+ *
318
+ * @param runtime the TR MemoryPluginRuntime (captured at register() time)
319
+ */
320
+ export function createMemorySearchTool(runtime: TrMemoryPluginRuntimeLike): AgentToolLike {
321
+ return {
322
+ name: 'memory_search',
323
+ label: 'Memory Search',
324
+ // Description is adapted from memory-core's to reflect TR's encrypted
325
+ // vault model (the agent calls memory_search which decrypts on the fly;
326
+ // it never sees files). The `corpus` param is accepted for schema parity
327
+ // but TR has no wiki/sessions supplements — only memory.
328
+ description:
329
+ 'Mandatory recall step: search the user’s encrypted TotalReclaw memory vault ' +
330
+ 'before answering questions about prior work, decisions, dates, people, ' +
331
+ 'preferences, or todos. Returns matching memories with citations you can ' +
332
+ 'deref via memory_get. If the response has disabled=true, memory recall ' +
333
+ 'is unavailable; tell the user and include the warning/action guidance.',
334
+ parameters: {
335
+ type: 'object',
336
+ properties: {
337
+ query: { type: 'string', description: 'The recall query.' },
338
+ maxResults: { type: 'integer', minimum: 1, description: 'Cap on returned hits.' },
339
+ minScore: { type: 'number', description: 'Drop hits below this score.' },
340
+ corpus: {
341
+ type: 'string',
342
+ enum: ['memory'],
343
+ description: 'TR only supports the memory corpus.',
344
+ },
345
+ },
346
+ required: ['query'],
347
+ additionalProperties: false,
348
+ },
349
+ execute: async (_toolCallId, params) => {
350
+ const raw = asParamsRecord(params);
351
+ // query is required — readStringParam throws on missing, caught below.
352
+ let query: string;
353
+ try {
354
+ const q = readStringParam(raw, 'query', { required: true });
355
+ if (q === undefined) throw new Error('query is required');
356
+ query = q;
357
+ } catch (e) {
358
+ // A missing required param is a caller error — surface it as a
359
+ // disabled result so the agent sees structured feedback rather than
360
+ // a thrown exception out of the tool boundary.
361
+ const msg = e instanceof Error ? e.message : String(e);
362
+ return jsonResult(buildUnavailableResult(msg));
363
+ }
364
+ const maxResults = readPositiveIntegerParam(raw, 'maxResults');
365
+ const minScore = readFiniteNumberParam(raw, 'minScore');
366
+
367
+ let resolved;
368
+ try {
369
+ // NOTE: memory-core passes `undefined` here normally (and `"cli"` for
370
+ // one-shot CLI runs); TR's runtime currently ignores `purpose`, so
371
+ // this is a no-op today. Kept as "search" for parity with the
372
+ // memory-core shape; flagged for correctness if Task 2.7 ever keys
373
+ // behavior off `purpose`.
374
+ resolved = await runtime.getMemorySearchManager({ purpose: 'search' });
375
+ } catch (e) {
376
+ const msg = e instanceof Error ? e.message : String(e);
377
+ return jsonResult(buildUnavailableResult(msg));
378
+ }
379
+ if (!resolved.manager) {
380
+ return jsonResult(buildUnavailableResult(resolved.error));
381
+ }
382
+
383
+ try {
384
+ const results = await resolved.manager.search(query, {
385
+ ...(maxResults !== undefined ? { maxResults } : {}),
386
+ ...(minScore !== undefined ? { minScore } : {}),
387
+ });
388
+ // Payload shape mirrors memory-core: { results, provider?, mode? }.
389
+ // TR's adapter is its own backend; we report provider:'totalreclaw'
390
+ // so the agent's diagnostics attribute hits correctly.
391
+ return jsonResult({
392
+ results,
393
+ provider: 'totalreclaw',
394
+ mode: 'builtin',
395
+ });
396
+ } catch (e) {
397
+ const msg = e instanceof Error ? e.message : String(e);
398
+ return jsonResult(buildUnavailableResult(msg));
399
+ }
400
+ },
401
+ };
402
+ }
403
+
404
+ // ---------------------------------------------------------------------------
405
+ // createMemoryGetTool
406
+ // ---------------------------------------------------------------------------
407
+
408
+ /**
409
+ * The memory_get tool factory. Returns a tool object structurally compatible
410
+ * with OpenClaw's AnyAgentTool, delegating to the TR MemorySearchManager's
411
+ * content-read method.
412
+ *
413
+ * Wiring (Task 2.7):
414
+ * api.registerTool(() => createMemoryGetTool(runtime), { names: ["memory_get"] });
415
+ *
416
+ * Handler contract:
417
+ * - Resolves the manager via `runtime.getMemorySearchManager({purpose:"status"})`.
418
+ * (memory-core uses purpose:"status" for reads; we match that.)
419
+ * - On { manager: null, error }: returns a disabled result keyed on the
420
+ * requested path so the agent can correlate.
421
+ * - On a thrown read error: returns a disabled result keyed on the path.
422
+ * - On success: forwards the manager's read result in an AgentToolResult.
423
+ *
424
+ * The read is a direct `manager.readFile(...)` call — `readFile` is the
425
+ * canonical method name on the TR adapter (TrMemorySearchManager). This file
426
+ * holds no network token, so the literal method name does not trip the
427
+ * scanner's per-file exfil rule (which requires env+net co-occurrence).
428
+ *
429
+ * @param runtime the TR MemoryPluginRuntime (captured at register() time)
430
+ */
431
+ export function createMemoryGetTool(runtime: TrMemoryPluginRuntimeLike): AgentToolLike {
432
+ return {
433
+ name: 'memory_get',
434
+ label: 'Memory Get',
435
+ description:
436
+ 'Read a single memory in full from the user’s encrypted TotalReclaw memory ' +
437
+ 'vault by its citation/path (as returned by memory_search). Supports ' +
438
+ 'optional from/lines pagination for large memories. Use this to pull the ' +
439
+ 'full text of a hit you found via memory_search. If the response has ' +
440
+ 'disabled=true, the read failed; surface the error to the user.',
441
+ parameters: {
442
+ type: 'object',
443
+ properties: {
444
+ path: { type: 'string', description: 'The memory citation/path to dereference.' },
445
+ from: { type: 'integer', minimum: 1, description: '1-indexed start line.' },
446
+ lines: { type: 'integer', minimum: 1, description: 'Max lines to return.' },
447
+ corpus: {
448
+ type: 'string',
449
+ enum: ['memory'],
450
+ description: 'TR only supports the memory corpus.',
451
+ },
452
+ },
453
+ required: ['path'],
454
+ additionalProperties: false,
455
+ },
456
+ execute: async (_toolCallId, params) => {
457
+ const raw = asParamsRecord(params);
458
+ let relPath: string;
459
+ try {
460
+ const p = readStringParam(raw, 'path', { required: true });
461
+ if (p === undefined) throw new Error('path is required');
462
+ relPath = p;
463
+ } catch (e) {
464
+ const msg = e instanceof Error ? e.message : String(e);
465
+ return jsonResult({ path: '', text: '', disabled: true, error: msg });
466
+ }
467
+ const from = readPositiveIntegerParam(raw, 'from');
468
+ const lines = readPositiveIntegerParam(raw, 'lines');
469
+
470
+ let resolved;
471
+ try {
472
+ resolved = await runtime.getMemorySearchManager({ purpose: 'status' });
473
+ } catch (e) {
474
+ const msg = e instanceof Error ? e.message : String(e);
475
+ return jsonResult({ path: relPath, text: '', disabled: true, error: msg });
476
+ }
477
+ if (!resolved.manager) {
478
+ return jsonResult({
479
+ path: relPath,
480
+ text: '',
481
+ disabled: true,
482
+ error: resolved.error ?? 'memory search unavailable',
483
+ });
484
+ }
485
+
486
+ try {
487
+ const result = await resolved.manager.readFile({
488
+ relPath,
489
+ ...(from !== undefined ? { from } : {}),
490
+ ...(lines !== undefined ? { lines } : {}),
491
+ });
492
+ return jsonResult(result);
493
+ } catch (e) {
494
+ const msg = e instanceof Error ? e.message : String(e);
495
+ return jsonResult({ path: relPath, text: '', disabled: true, error: msg });
496
+ }
497
+ },
498
+ };
499
+ }