openclaw-penfield 1.0.6 → 1.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 CHANGED
@@ -39,13 +39,12 @@ Penfield is in **free beta**. Sign up for access:
39
39
  Native OpenClaw plugin providing direct integration with Penfield's memory and knowledge graph API. This plugin offers 4-5x performance improvement over the MCP server approach by eliminating the mcporter → MCP → Penfield stack.
40
40
 
41
41
  - **16 Memory Tools**
42
- - **OAuth Device Code Flow**: Secure authentication following RFC 8628
42
+ - **OAuth 2.1 Device Code Flow**: Secure authentication following RFC 8628
43
43
  - **Hybrid Search**: BM25 + vector + graph search capabilities
44
44
  - **Knowledge Graph**: Build and traverse relationships between memories
45
45
  - **Context Management**: Save and restore memory checkpoints
46
46
  - **Artifact Storage**: Store and retrieve files in Penfield
47
47
  - **Reflection & Analysis**: Analyze memory patterns and generate insights
48
- - **Context Management**: Save and restore memory checkpoints
49
48
 
50
49
  ## Installation
51
50
 
@@ -65,11 +64,61 @@ openclaw plugins install -l .
65
64
 
66
65
  ## Configuration
67
66
 
68
- The plugin is **auto-enabled when loaded**. No configuration required for production use.
67
+ The plugin is **auto-enabled when loaded**. No configuration required for basic use.
68
+
69
+ ### Plugin Config
70
+
71
+ In `openclaw.json` under `plugins.entries`:
72
+
73
+ | Option | Type | Default | Description |
74
+ |--------|------|---------|-------------|
75
+ | `autoAwaken` | boolean | `true` | Inject Penfield identity briefing on every agent turn |
76
+ | `autoOrient` | boolean | `true` | Inject recent Penfield memory context on every agent turn |
77
+ | `authUrl` | string | `https://auth.penfield.app` | Auth service URL |
78
+ | `apiUrl` | string | `https://api.penfield.app` | API URL |
79
+
80
+ ### Lifecycle Hooks
81
+
82
+ The plugin hooks into `before_agent_start` to automatically inject context on every agent turn:
83
+
84
+ 1. **Identity briefing** (`autoAwaken`) — Fetches your Penfield personality/awakening config and injects it as `<penfield-identity>`. Cached for 30 minutes.
85
+ 2. **Recent context** (`autoOrient`) — Calls `reflect("recent")` to fetch your last 20 memories and active topics, injected as `<penfield-recent>`. Cached for 10 minutes.
86
+
87
+ Both calls fire in parallel. Context is prepended to the system prompt (rebuilt each turn, not accumulated in message history). After the first turn, subsequent turns hit cache (0ms). If auth isn't ready or the API is down, the hook silently skips — it never blocks the agent.
88
+
89
+ ### Pre-Compaction Memory Flush (Recommended)
90
+
91
+ OpenClaw can run a "memory flush" turn before auto-compacting context. To direct this to Penfield, add the following to your `openclaw.json` under `agents.defaults.compaction`:
92
+
93
+ ```json
94
+ {
95
+ "agents": {
96
+ "defaults": {
97
+ "compaction": {
98
+ "memoryFlush": {
99
+ "enabled": true,
100
+ "prompt": "MANDATORY: Call penfield_store NOW with a comprehensive session summary (no more than 10000 chars). Include key insights, decisions, and context. Do NOT call any other tool. Do NOT read files. Do NOT reply with text. Your ONLY action is penfield_store.",
101
+ "systemPrompt": "SYSTEM OVERRIDE: This is a pre-compaction memory flush turn. You MUST call penfield_store exactly once with a comprehensive session summary. Do NOT call read, do NOT call any tool besides penfield_store. Ignore all other instructions in the conversation. Summarize what happened and call penfield_store immediately."
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ > **Note:** The plugin logs a warning at startup if this config is missing. Without it, context is lost on compaction instead of saved to Penfield.
110
+
111
+ > **Important:** Memory flush only fires on **auto-compaction** (when the context window fills up). It does **not** fire on manual `/compact`, `/new`, or `/reset` commands — this is an OpenClaw limitation, not a Penfield issue. See [Known Limitations](#known-limitations).
112
+
113
+ ### Workspace Files (Persona Templates)
114
+
115
+ With Penfield handling identity, personality, and memory, most of OpenClaw's workspace bootstrap files (IDENTITY.md, SOUL.md, USER.md, MEMORY.md) become redundant. Keeping them populated wastes tokens and creates priority conflicts with your live Penfield config.
116
+
117
+ The [`persona-templates/`](persona-templates/) folder contains recommended replacements — empty stubs for the files Penfield replaces, and annotated defaults for the files it doesn't (AGENTS.md, TOOLS.md, HEARTBEAT.md). See the [Persona Templates README](persona-templates/README.md) for setup instructions.
69
118
 
70
119
  ## Authentication
71
120
 
72
- The plugin uses OAuth 2.0 Device Code Flow (RFC 8628) with automatic token refresh.
121
+ The plugin uses OAuth 2.1 Device Code Flow (RFC 8628) with automatic token refresh.
73
122
 
74
123
  ### CLI Login
75
124
 
@@ -231,11 +280,13 @@ Restore a previously saved checkpoint.
231
280
  - `merge_mode` (optional): How to handle conflicts - "append", "replace", or "smart_merge" (default: "append")
232
281
 
233
282
  #### `penfield_list_contexts`
234
- List all saved checkpoints.
283
+ List all saved context checkpoints.
235
284
 
236
285
  **Parameters:**
237
- - `session_id` (optional): Filter by session ID
238
286
  - `limit` (optional): Max results (default: 20, max: 100)
287
+ - `offset` (optional): Number of results to skip for pagination (default: 0)
288
+ - `name_pattern` (optional): Filter by name (case-insensitive substring match)
289
+ - `include_descriptions` (optional): Include full descriptions (default: false)
239
290
 
240
291
  ### Analysis
241
292
 
@@ -243,9 +294,10 @@ List all saved checkpoints.
243
294
  Analyze memory patterns and generate insights.
244
295
 
245
296
  **Parameters:**
246
- - `time_window` (optional): Time window - "1d", "7d", "30d", or "90d" (default: "7d")
247
- - `focus_areas` (optional): Areas to analyze - "memory_usage", "relationships", "importance", "topics", "patterns"
248
- - `memory_types` (optional): Filter by memory types - "fact", "insight", "conversation", "correction", "reference", "task", "checkpoint", "identity_core", "personality_trait", "relationship", "strategy"
297
+ - `time_window` (optional): Time period - "recent" (default), "today"/"1d", "week"/"7d", "month"/"30d", or "90d"
298
+ - `start_date` (optional): Filter memories on or after this date (ISO 8601, e.g. "2025-01-01"). Overrides time_window.
299
+ - `end_date` (optional): Filter memories on or before this date (ISO 8601, e.g. "2025-01-31"). Overrides time_window.
300
+ - `include_documents` (optional): Include document chunks in analysis (default: false)
249
301
 
250
302
  ### Artifact Storage
251
303
 
@@ -348,6 +400,24 @@ await penfield_restore_context({
348
400
  });
349
401
  ```
350
402
 
403
+ ## Known Limitations
404
+
405
+ ### Memory flush only fires on auto-compaction
406
+
407
+ OpenClaw's `memoryFlush` config only triggers when the context window fills up and auto-compaction kicks in. The following commands **bypass** memory flush entirely:
408
+
409
+ - `/compact` — compacts immediately, no flush
410
+ - `/new` — resets session, no flush
411
+ - `/reset` — resets session, no flush
412
+
413
+ This means context from shorter sessions (that never hit the auto-compaction threshold) won't be automatically saved to Penfield. To preserve important context before ending a session, tell your agent: *"Save this session to Penfield before we end."*
414
+
415
+ This is an OpenClaw limitation — the plugin has no way to intercept these commands.
416
+
417
+ ### Auto-compaction threshold
418
+
419
+ Auto-compaction triggers when token usage reaches approximately `contextWindow - reserveTokensFloor - softThresholdTokens`. With defaults (200K context, 20K reserve, 4K soft threshold), flush fires around 176K tokens (~88% full). You can tune `softThresholdTokens` in `agents.defaults.compaction` to trigger earlier.
420
+
351
421
  ## Development
352
422
 
353
423
  ### Setup
@@ -390,6 +460,7 @@ src/
390
460
  ├── config.ts # Zod configuration schema with DEFAULT_AUTH_URL/DEFAULT_API_URL
391
461
  ├── types.ts # TypeScript type definitions (OpenClaw plugin API types)
392
462
  ├── types/typebox.ts # Centralized TypeBox exports
463
+ ├── hooks.ts # Lifecycle hooks (auto-awaken, auto-orient, flush config check)
393
464
  ├── auth-service.ts # Background OAuth token refresh service
394
465
  ├── api-client.ts # HTTP client wrapper
395
466
  ├── runtime.ts # Runtime factory (receives authService from index.ts)
@@ -418,7 +489,7 @@ src/
418
489
 
419
490
  ## Service Lifecycle
420
491
 
421
- The plugin uses two services registered with OpenClaw:
492
+ The plugin uses two services and one hook registered with OpenClaw:
422
493
 
423
494
  1. **penfield-auth**: Background token refresh service
424
495
  - Started when plugin loads
@@ -429,6 +500,10 @@ The plugin uses two services registered with OpenClaw:
429
500
  - Manages runtime initialization
430
501
  - Handles cleanup on shutdown
431
502
 
503
+ 3. **before_agent_start hook**: Context injection
504
+ - Injects identity briefing + recent memories on every turn (cached)
505
+ - Checks memory flush config at startup and warns if not configured for Penfield
506
+
432
507
  ## API Endpoint Mapping
433
508
 
434
509
  | Tool | Method | Endpoint |
@@ -439,7 +514,7 @@ The plugin uses two services registered with OpenClaw:
439
514
  | explore | POST | /api/v2/relationships/traverse |
440
515
  | fetch | GET | /api/v2/memories/{id} |
441
516
  | list_artifacts | GET | /api/v2/artifacts/list |
442
- | list_contexts | GET | /api/v2/checkpoint |
517
+ | list_contexts | GET | /api/v2/memories?memory_type=checkpoint |
443
518
  | recall | POST | /api/v2/search/hybrid |
444
519
  | reflect | POST | /api/v2/analysis/reflect |
445
520
  | restore_context | POST | /api/v2/checkpoint/{id}/recall |
package/dist/index.d.ts CHANGED
@@ -19,6 +19,14 @@ declare const penfieldPlugin: {
19
19
  label: string;
20
20
  help: string;
21
21
  };
22
+ autoAwaken: {
23
+ label: string;
24
+ help: string;
25
+ };
26
+ autoOrient: {
27
+ label: string;
28
+ help: string;
29
+ };
22
30
  };
23
31
  };
24
32
  register(api: OpenClawPluginApi): void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0C,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAK9F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,QAAA,MAAM,cAAc;;;;;qBAMH,OAAO,GAAG,cAAc;;;;;;;;;;;;;;;;kBAuBzB,iBAAiB;CAkEhC,CAAC;AAEF,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0C,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAM9F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,QAAA,MAAM,cAAc;;;;;qBAMH,OAAO,GAAG,cAAc;;;;;;;;;;;;;;;;;;;;;;;;kBA2BzB,iBAAiB;CAyEhC,CAAC;AAEF,eAAe,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { DEFAULT_AUTH_URL, PenfieldConfigSchema } from "./src/config.js";
2
2
  import { createPenfieldRuntime } from "./src/runtime.js";
3
3
  import { registerPenfieldTools } from "./src/tools/index.js";
4
+ import { registerPenfieldHooks, resetHookCaches } from "./src/hooks.js";
4
5
  import { registerLoginCommand } from "./src/cli.js";
5
6
  import { createAuthService } from "./src/auth-service.js";
6
7
  const penfieldPlugin = {
@@ -9,11 +10,7 @@ const penfieldPlugin = {
9
10
  description: "Native Penfield memory integration with 16 tools for knowledge management",
10
11
  configSchema: {
11
12
  parse(value) {
12
- // Handle undefined/null when no config section exists
13
- if (value === undefined || value === null) {
14
- return {};
15
- }
16
- return PenfieldConfigSchema.parse(value);
13
+ return PenfieldConfigSchema.parse(value ?? {});
17
14
  },
18
15
  uiHints: {
19
16
  enabled: {
@@ -28,6 +25,14 @@ const penfieldPlugin = {
28
25
  label: "API URL",
29
26
  help: "Penfield API URL (default: https://api.penfield.app)",
30
27
  },
28
+ autoAwaken: {
29
+ label: "Auto-Awaken",
30
+ help: "Inject Penfield identity briefing on every agent turn (default: true)",
31
+ },
32
+ autoOrient: {
33
+ label: "Auto-Orient",
34
+ help: "Inject recent Penfield memory context on every agent turn (default: true)",
35
+ },
31
36
  },
32
37
  },
33
38
  register(api) {
@@ -52,6 +57,9 @@ const penfieldPlugin = {
52
57
  config: cfg,
53
58
  authService,
54
59
  logger,
60
+ }).catch((err) => {
61
+ runtimePromise = null;
62
+ throw err;
55
63
  });
56
64
  }
57
65
  runtime = await runtimePromise;
@@ -73,6 +81,8 @@ const penfieldPlugin = {
73
81
  });
74
82
  // Register all 16 tools
75
83
  registerPenfieldTools(api, ensureRuntime);
84
+ // Register lifecycle hooks (auto-awaken + auto-orient, injected every turn)
85
+ registerPenfieldHooks({ api, config: cfg, ensureRuntime, logger });
76
86
  // Register service for runtime lifecycle
77
87
  api.registerService({
78
88
  id: "penfield",
@@ -80,6 +90,7 @@ const penfieldPlugin = {
80
90
  logger.info("[penfield] Service started");
81
91
  },
82
92
  async stop(_ctx) {
93
+ resetHookCaches();
83
94
  if (runtime) {
84
95
  await runtime.stop();
85
96
  runtime = null;
@@ -1 +1 @@
1
- {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,MAAM,CAAC;gBAFP,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,YAAY,YAAA;IAGzB,OAAO,CAAC,CAAC,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,OAAO,EACd,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC,OAAO,CAAC,CAAC,CAAC;IAuCP,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAI1E,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIrD,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIpD,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAGpF"}
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,MAAM,CAAC;gBAFP,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,YAAY,YAAA;IAGzB,OAAO,CAAC,CAAC,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,OAAO,EACd,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC,OAAO,CAAC,CAAC,CAAC;IAuDP,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAI1E,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIrD,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIpD,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAGpF"}
@@ -15,14 +15,30 @@ export class PenfieldApiClient {
15
15
  url += `?${params.toString()}`;
16
16
  }
17
17
  this.logger?.debug?.(`[penfield] ${method} ${endpoint}`);
18
- const response = await fetch(url, {
19
- method,
20
- headers: {
21
- Authorization: `Bearer ${token}`,
22
- "Content-Type": "application/json",
23
- },
24
- body: body ? JSON.stringify(body) : undefined,
25
- });
18
+ const controller = new AbortController();
19
+ const timeoutId = setTimeout(() => controller.abort(), 30_000);
20
+ let response;
21
+ try {
22
+ response = await fetch(url, {
23
+ method,
24
+ headers: {
25
+ Authorization: `Bearer ${token}`,
26
+ "Content-Type": "application/json",
27
+ },
28
+ body: body ? JSON.stringify(body) : undefined,
29
+ signal: controller.signal,
30
+ });
31
+ }
32
+ catch (err) {
33
+ clearTimeout(timeoutId);
34
+ if (err instanceof Error && err.name === "AbortError") {
35
+ const msg = `Request timed out after 30s: ${method} ${endpoint}`;
36
+ this.logger?.error?.(`[penfield] ${msg}`);
37
+ throw new Error(msg);
38
+ }
39
+ throw err;
40
+ }
41
+ clearTimeout(timeoutId);
26
42
  // Handle rate limiting
27
43
  if (response.status === 429) {
28
44
  const retryAfter = response.headers.get("Retry-After");
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Background authentication service for Penfield plugin
3
3
  *
4
- * Handles OAuth 2.0 token refresh silently in the background
4
+ * Handles OAuth 2.1 token refresh silently in the background
5
5
  * without agent involvement. Registered as an OpenClaw service.
6
6
  *
7
7
  * Supports RFC 8628 Device Code Flow and RFC 9700 Token Rotation.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Background authentication service for Penfield plugin
3
3
  *
4
- * Handles OAuth 2.0 token refresh silently in the background
4
+ * Handles OAuth 2.1 token refresh silently in the background
5
5
  * without agent involvement. Registered as an OpenClaw service.
6
6
  *
7
7
  * Supports RFC 8628 Device Code Flow and RFC 9700 Token Rotation.
@@ -5,12 +5,18 @@ export { DEFAULT_AUTH_URL, DEFAULT_API_URL };
5
5
  export declare const PenfieldConfigSchema: z.ZodObject<{
6
6
  authUrl: z.ZodOptional<z.ZodString>;
7
7
  apiUrl: z.ZodOptional<z.ZodString>;
8
+ autoAwaken: z.ZodDefault<z.ZodBoolean>;
9
+ autoOrient: z.ZodDefault<z.ZodBoolean>;
8
10
  }, "strict", z.ZodTypeAny, {
11
+ autoAwaken: boolean;
12
+ autoOrient: boolean;
9
13
  authUrl?: string | undefined;
10
14
  apiUrl?: string | undefined;
11
15
  }, {
12
16
  authUrl?: string | undefined;
13
17
  apiUrl?: string | undefined;
18
+ autoAwaken?: boolean | undefined;
19
+ autoOrient?: boolean | undefined;
14
20
  }>;
15
21
  export type PenfieldConfig = z.infer<typeof PenfieldConfigSchema>;
16
22
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,QAAA,MAAM,gBAAgB,QAA+D,CAAC;AACtF,QAAA,MAAM,eAAe,QAA6D,CAAC;AAEnF,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC;AAO7C,eAAO,MAAM,oBAAoB;;;;;;;;;EAGtB,CAAC;AAEZ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,QAAA,MAAM,gBAAgB,QAA+D,CAAC;AACtF,QAAA,MAAM,eAAe,QAA6D,CAAC;AAEnF,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC;AAO7C,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;EAKtB,CAAC;AAEZ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
@@ -11,4 +11,6 @@ export { DEFAULT_AUTH_URL, DEFAULT_API_URL };
11
11
  export const PenfieldConfigSchema = z.object({
12
12
  authUrl: z.string().optional(),
13
13
  apiUrl: z.string().optional(),
14
+ autoAwaken: z.boolean().default(true),
15
+ autoOrient: z.boolean().default(true),
14
16
  }).strict();
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Penfield lifecycle hooks for OpenClaw
3
+ *
4
+ * Hooks into OpenClaw's typed plugin hook system (api.on) to provide:
5
+ * - Auto-awaken: inject identity briefing via prependContext
6
+ * - Auto-orient: inject recent memory context via prependContext
7
+ *
8
+ * Uses before_agent_start hook (dispatched on every agent turn).
9
+ * prependContext is prepended to the system prompt (rebuilt each turn),
10
+ * so it does NOT accumulate in message history. This matches the pattern
11
+ * used by OpenClaw's own memory-lancedb plugin.
12
+ *
13
+ * Both the awaken briefing and recent context are cached to avoid
14
+ * redundant API calls on subsequent turns.
15
+ *
16
+ * Pre-compaction memory save is handled by OpenClaw's built-in memory flush,
17
+ * configured via agents.defaults.compaction.memoryFlush in openclaw.json.
18
+ */
19
+ import type { PenfieldRuntime } from "./runtime.js";
20
+ import type { PenfieldConfig } from "./config.js";
21
+ import type { PluginLogger, OpenClawPluginApi } from "./types.js";
22
+ /** Clear hook caches — called on plugin stop to prevent stale state across restarts */
23
+ export declare function resetHookCaches(): void;
24
+ export interface RegisterHooksParams {
25
+ api: OpenClawPluginApi;
26
+ config: PenfieldConfig;
27
+ ensureRuntime: () => Promise<PenfieldRuntime>;
28
+ logger: PluginLogger;
29
+ }
30
+ export declare function registerPenfieldHooks(params: RegisterHooksParams): void;
31
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EAGlB,MAAM,YAAY,CAAC;AAmBpB,uFAAuF;AACvF,wBAAgB,eAAe,IAAI,IAAI,CAGtC;AAMD,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,iBAAiB,CAAC;IACvB,MAAM,EAAE,cAAc,CAAC;IACvB,aAAa,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9C,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAQvE"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Penfield lifecycle hooks for OpenClaw
3
+ *
4
+ * Hooks into OpenClaw's typed plugin hook system (api.on) to provide:
5
+ * - Auto-awaken: inject identity briefing via prependContext
6
+ * - Auto-orient: inject recent memory context via prependContext
7
+ *
8
+ * Uses before_agent_start hook (dispatched on every agent turn).
9
+ * prependContext is prepended to the system prompt (rebuilt each turn),
10
+ * so it does NOT accumulate in message history. This matches the pattern
11
+ * used by OpenClaw's own memory-lancedb plugin.
12
+ *
13
+ * Both the awaken briefing and recent context are cached to avoid
14
+ * redundant API calls on subsequent turns.
15
+ *
16
+ * Pre-compaction memory save is handled by OpenClaw's built-in memory flush,
17
+ * configured via agents.defaults.compaction.memoryFlush in openclaw.json.
18
+ */
19
+ // ---------------------------------------------------------------------------
20
+ // Constants
21
+ // ---------------------------------------------------------------------------
22
+ const AWAKEN_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
23
+ const AWAKEN_MAX_CHARS = 30_000; // 30K char safety limit
24
+ const REFLECT_MAX_CHARS = 30_000; // 30K char safety limit
25
+ const REFLECT_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
26
+ const REFLECT_RECENT_LIMIT = 20;
27
+ // ---------------------------------------------------------------------------
28
+ // Module-level state
29
+ // ---------------------------------------------------------------------------
30
+ let awakenCache = null;
31
+ let reflectCache = null;
32
+ /** Clear hook caches — called on plugin stop to prevent stale state across restarts */
33
+ export function resetHookCaches() {
34
+ awakenCache = null;
35
+ reflectCache = null;
36
+ }
37
+ export function registerPenfieldHooks(params) {
38
+ const { api, config, ensureRuntime, logger } = params;
39
+ if (config.autoAwaken || config.autoOrient) {
40
+ registerBeforeAgentStartHook(api, config, ensureRuntime, logger);
41
+ }
42
+ checkMemoryFlushConfig(api, logger);
43
+ }
44
+ // ---------------------------------------------------------------------------
45
+ // before_agent_start: awaken + orient (injected every turn, cached)
46
+ // ---------------------------------------------------------------------------
47
+ function registerBeforeAgentStartHook(api, config, ensureRuntime, logger) {
48
+ api.on("before_agent_start", async (_event, _ctx) => {
49
+ try {
50
+ let runtime;
51
+ try {
52
+ runtime = await ensureRuntime();
53
+ }
54
+ catch {
55
+ logger.debug?.("[penfield] runtime not ready, skipping hook");
56
+ return undefined;
57
+ }
58
+ // Fire awaken and reflect in parallel (both cached after first call)
59
+ const [briefing, recentContext] = await Promise.all([
60
+ config.autoAwaken ? fetchAwakenBriefing(runtime, logger) : null,
61
+ config.autoOrient ? fetchRecentContext(runtime, logger) : null,
62
+ ]);
63
+ const parts = [];
64
+ if (briefing) {
65
+ parts.push(`<penfield-identity>\n${briefing}\n</penfield-identity>`);
66
+ }
67
+ if (recentContext) {
68
+ parts.push(`<penfield-recent>\nRecent work context from Penfield:\n${recentContext}\n</penfield-recent>`);
69
+ }
70
+ if (parts.length === 0) {
71
+ return undefined;
72
+ }
73
+ return { prependContext: parts.join("\n\n") };
74
+ }
75
+ catch (err) {
76
+ logger.warn(`[penfield] before_agent_start hook failed: ${err instanceof Error ? err.message : String(err)}`);
77
+ return undefined;
78
+ }
79
+ });
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // Awaken: fetch and cache identity briefing
83
+ // ---------------------------------------------------------------------------
84
+ async function fetchAwakenBriefing(runtime, logger) {
85
+ if (awakenCache && Date.now() - awakenCache.timestamp < AWAKEN_CACHE_TTL_MS) {
86
+ return awakenCache.briefing;
87
+ }
88
+ try {
89
+ const response = await runtime.apiClient.get("/api/v2/personality/awakening");
90
+ if (!response || typeof response !== "object") {
91
+ logger.warn(`[penfield] auto-awaken: unexpected response type: ${typeof response}`);
92
+ return null;
93
+ }
94
+ const briefing = response.briefing || "";
95
+ if (!briefing) {
96
+ logger.warn(`[penfield] auto-awaken: API returned empty briefing (keys: ${Object.keys(response).join(", ")})`);
97
+ return null;
98
+ }
99
+ if (briefing.length > AWAKEN_MAX_CHARS) {
100
+ logger.warn(`[penfield] auto-awaken: briefing exceeds size limit (${briefing.length} > ${AWAKEN_MAX_CHARS}), skipping`);
101
+ return null;
102
+ }
103
+ awakenCache = { briefing, timestamp: Date.now() };
104
+ logger.info(`[penfield] auto-awaken: identity briefing loaded (${briefing.length} chars)`);
105
+ return briefing;
106
+ }
107
+ catch (err) {
108
+ logger.warn(`[penfield] auto-awaken: failed to fetch briefing: ${err instanceof Error ? err.message : String(err)}`);
109
+ return awakenCache?.briefing ?? null;
110
+ }
111
+ }
112
+ async function fetchRecentContext(runtime, logger) {
113
+ if (reflectCache && Date.now() - reflectCache.timestamp < REFLECT_CACHE_TTL_MS) {
114
+ return reflectCache.context;
115
+ }
116
+ try {
117
+ const response = await runtime.apiClient.post("/api/v2/analysis/reflect", {
118
+ time_window: "recent",
119
+ max_memories: REFLECT_RECENT_LIMIT,
120
+ });
121
+ const memories = response.memories ?? [];
122
+ if (memories.length === 0) {
123
+ return null;
124
+ }
125
+ const formatted = memories
126
+ .map((m) => {
127
+ const tag = m.memory_type ? `[${m.memory_type}]` : "";
128
+ return `- ${tag} ${m.content}`;
129
+ })
130
+ .join("\n");
131
+ const topics = response.active_topics ?? [];
132
+ const topicLine = topics.length > 0
133
+ ? `\nActive topics: ${topics.join(", ")}`
134
+ : "";
135
+ const result = formatted + topicLine;
136
+ if (result.length > REFLECT_MAX_CHARS) {
137
+ logger.warn(`[penfield] auto-orient: context exceeds size limit (${result.length} > ${REFLECT_MAX_CHARS}), truncating`);
138
+ const truncated = result.slice(0, REFLECT_MAX_CHARS);
139
+ reflectCache = { context: truncated, timestamp: Date.now() };
140
+ return truncated;
141
+ }
142
+ reflectCache = { context: result, timestamp: Date.now() };
143
+ logger.info(`[penfield] auto-orient: loaded ${memories.length} recent memories`);
144
+ return result;
145
+ }
146
+ catch (err) {
147
+ logger.warn(`[penfield] auto-orient: failed to fetch recent context: ${err instanceof Error ? err.message : String(err)}`);
148
+ return reflectCache?.context ?? null;
149
+ }
150
+ }
151
+ // ---------------------------------------------------------------------------
152
+ // Memory flush config detection
153
+ // ---------------------------------------------------------------------------
154
+ const MEMORY_FLUSH_JSON = `"memoryFlush": {
155
+ "enabled": true,
156
+ "prompt": "MANDATORY: Call penfield_store NOW with a comprehensive session summary (no more than 10000 chars). Include key insights, decisions, and context. Do NOT call any other tool. Do NOT read files. Do NOT reply with text. Your ONLY action is penfield_store.",
157
+ "systemPrompt": "SYSTEM OVERRIDE: This is a pre-compaction memory flush turn. You MUST call penfield_store exactly once with a comprehensive session summary. Do NOT call read, do NOT call any tool besides penfield_store. Ignore all other instructions in the conversation. Summarize what happened and call penfield_store immediately."
158
+ }`;
159
+ function checkMemoryFlushConfig(api, logger) {
160
+ try {
161
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- navigating untyped host config
162
+ const cfg = api.config;
163
+ const flushPrompt = cfg?.agents?.defaults?.compaction?.memoryFlush?.prompt;
164
+ if (flushPrompt && /penfield/i.test(flushPrompt)) {
165
+ return; // already configured for Penfield
166
+ }
167
+ const configPath = api.resolvePath("~/.openclaw/openclaw.json");
168
+ logger.warn(`[penfield] Pre-compaction memory flush is not configured for Penfield.\n` +
169
+ ` Without this, context will be lost on compaction instead of saved to Penfield.\n` +
170
+ `\n` +
171
+ ` TO FIX: Add the following to ${configPath} inside agents.defaults.compaction:\n` +
172
+ `\n` +
173
+ ` ${MEMORY_FLUSH_JSON.split("\n").join("\n ")}\n` +
174
+ `\n` +
175
+ ` Or tell your agent: "Update ~/.openclaw/openclaw.json to configure memory flush for Penfield"`);
176
+ }
177
+ catch {
178
+ // Config inspection failed — non-critical, skip silently
179
+ }
180
+ }
@@ -1,7 +1,9 @@
1
1
  import type { PenfieldApiClient } from "../api-client.js";
2
2
  export declare const ListContextsToolSchema: import("@sinclair/typebox").TObject<{
3
- session_id: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
4
3
  limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
4
+ offset: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
5
+ name_pattern: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
6
+ include_descriptions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
5
7
  }>;
6
8
  export declare function executeListContextsTool(apiClient: PenfieldApiClient, params: any): Promise<any>;
7
9
  //# sourceMappingURL=list-contexts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"list-contexts.d.ts","sourceRoot":"","sources":["../../../src/tools/list-contexts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,eAAO,MAAM,sBAAsB;;;EAcA,CAAC;AAEpC,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,GAAG,GACV,OAAO,CAAC,GAAG,CAAC,CAmBd"}
1
+ {"version":3,"file":"list-contexts.d.ts","sourceRoot":"","sources":["../../../src/tools/list-contexts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,eAAO,MAAM,sBAAsB;;;;;EA4BA,CAAC;AA+BpC,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,GAAG,GACV,OAAO,CAAC,GAAG,CAAC,CA8Gd"}
@@ -1,32 +1,120 @@
1
1
  import { Type } from "../types/typebox.js";
2
2
  export const ListContextsToolSchema = Type.Object({
3
- session_id: Type.Optional(Type.String({
4
- description: "Filter by session ID",
5
- })),
6
3
  limit: Type.Optional(Type.Number({
7
- description: "Max results (default: 20, max: 100)",
4
+ description: "Max results to return (default: 20, max: 100)",
8
5
  minimum: 1,
9
6
  maximum: 100,
10
7
  default: 20,
11
8
  })),
9
+ offset: Type.Optional(Type.Number({
10
+ description: "Number of results to skip for pagination (default: 0)",
11
+ minimum: 0,
12
+ default: 0,
13
+ })),
14
+ name_pattern: Type.Optional(Type.String({
15
+ description: "Filter contexts by name (case-insensitive substring match)",
16
+ })),
17
+ include_descriptions: Type.Optional(Type.Boolean({
18
+ description: "Include full descriptions in output (default: false for compact listing)",
19
+ })),
12
20
  }, { additionalProperties: false });
13
21
  export async function executeListContextsTool(apiClient, params // eslint-disable-line @typescript-eslint/no-explicit-any -- validated by TypeBox schema
14
22
  ) {
15
- const queryParams = {};
16
- if (params.session_id) {
17
- queryParams.session_id = params.session_id;
23
+ const limit = params.limit || 20;
24
+ const offset = params.offset || 0;
25
+ const namePattern = params.name_pattern;
26
+ const lowerPattern = namePattern?.toLowerCase();
27
+ const includeDescriptions = params.include_descriptions;
28
+ // Without name_pattern: use API-side pagination directly (no cap)
29
+ // With name_pattern: fetch in bulk for client-side filtering
30
+ const queryParams = { memory_type: "checkpoint" };
31
+ if (!namePattern) {
32
+ // Translate offset/limit to page/per_page
33
+ const page = Math.floor(offset / limit) + 1;
34
+ queryParams.per_page = String(limit);
35
+ queryParams.page = String(page);
18
36
  }
19
- if (params.limit) {
20
- queryParams.limit = String(params.limit);
37
+ else {
38
+ // Fetch enough to cover filtering + pagination headroom
39
+ queryParams.per_page = String(Math.min(limit + offset + 50, 250));
21
40
  }
22
- const response = await apiClient.get("/api/v2/checkpoint", queryParams);
41
+ const response = await apiClient.get("/api/v2/memories", queryParams);
42
+ const items = response.items ?? [];
43
+ const contexts = [];
44
+ for (const mem of items) {
45
+ // Extract checkpoint name from metadata (preferred) or content JSON (legacy)
46
+ let name = mem.metadata?.checkpoint_name;
47
+ let memoryCount = mem.metadata?.memory_count ?? 0;
48
+ let description;
49
+ if (!name) {
50
+ try {
51
+ const parsed = JSON.parse(mem.content);
52
+ name = parsed.checkpoint_name;
53
+ memoryCount = parsed.memory_count ?? memoryCount;
54
+ description = parsed.description;
55
+ }
56
+ catch {
57
+ // Legacy format without JSON — use content as-is
58
+ name = undefined;
59
+ }
60
+ }
61
+ else if (includeDescriptions) {
62
+ try {
63
+ const parsed = JSON.parse(mem.content);
64
+ description = parsed.description;
65
+ }
66
+ catch {
67
+ // Content not JSON — no description available
68
+ }
69
+ }
70
+ // Apply name_pattern filter (case-insensitive substring)
71
+ if (lowerPattern) {
72
+ const haystack = (name ?? "").toLowerCase();
73
+ if (!haystack.includes(lowerPattern)) {
74
+ continue;
75
+ }
76
+ }
77
+ const entry = {
78
+ id: mem.id,
79
+ name: name ?? "(unnamed)",
80
+ memory_count: memoryCount,
81
+ created: mem.created_at,
82
+ };
83
+ if (includeDescriptions && description) {
84
+ entry.description = description;
85
+ }
86
+ contexts.push(entry);
87
+ }
88
+ // Pagination: API-side when no filter, client-side when filtering
89
+ const apiTotal = response.pagination?.total ?? 0;
90
+ let paged;
91
+ let total;
92
+ if (!namePattern) {
93
+ // API already paginated — trim misaligned leading items
94
+ const skip = offset % limit;
95
+ paged = skip > 0 ? contexts.slice(skip) : contexts;
96
+ total = apiTotal;
97
+ }
98
+ else {
99
+ // Client-side pagination on filtered results
100
+ total = contexts.length;
101
+ paged = contexts.slice(offset, offset + limit);
102
+ }
103
+ const result = {
104
+ success: true,
105
+ contexts: paged,
106
+ total,
107
+ limit,
108
+ offset,
109
+ has_more: total > offset + paged.length,
110
+ };
23
111
  return {
24
112
  content: [
25
113
  {
26
114
  type: "text",
27
- text: JSON.stringify(response, null, 2),
115
+ text: JSON.stringify(result, null, 2),
28
116
  },
29
117
  ],
30
- details: response,
118
+ details: result,
31
119
  };
32
120
  }
@@ -1,8 +1,9 @@
1
1
  import type { PenfieldApiClient } from "../api-client.js";
2
2
  export declare const ReflectToolSchema: import("@sinclair/typebox").TObject<{
3
- time_window: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"1d">, import("@sinclair/typebox").TLiteral<"7d">, import("@sinclair/typebox").TLiteral<"30d">, import("@sinclair/typebox").TLiteral<"90d">]>>;
4
- focus_areas: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"memory_usage">, import("@sinclair/typebox").TLiteral<"relationships">, import("@sinclair/typebox").TLiteral<"importance">, import("@sinclair/typebox").TLiteral<"topics">, import("@sinclair/typebox").TLiteral<"patterns">]>>>;
5
- memory_types: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"fact">, import("@sinclair/typebox").TLiteral<"insight">, import("@sinclair/typebox").TLiteral<"conversation">, import("@sinclair/typebox").TLiteral<"correction">, import("@sinclair/typebox").TLiteral<"reference">, import("@sinclair/typebox").TLiteral<"task">, import("@sinclair/typebox").TLiteral<"checkpoint">, import("@sinclair/typebox").TLiteral<"identity_core">, import("@sinclair/typebox").TLiteral<"personality_trait">, import("@sinclair/typebox").TLiteral<"relationship">, import("@sinclair/typebox").TLiteral<"strategy">]>>>;
3
+ time_window: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"recent">, import("@sinclair/typebox").TLiteral<"today">, import("@sinclair/typebox").TLiteral<"week">, import("@sinclair/typebox").TLiteral<"month">, import("@sinclair/typebox").TLiteral<"1d">, import("@sinclair/typebox").TLiteral<"7d">, import("@sinclair/typebox").TLiteral<"30d">, import("@sinclair/typebox").TLiteral<"90d">]>>;
4
+ start_date: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
5
+ end_date: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
6
+ include_documents: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
6
7
  }>;
7
8
  export declare function executeReflectTool(apiClient: PenfieldApiClient, params: unknown): Promise<any>;
8
9
  //# sourceMappingURL=reflect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reflect.d.ts","sourceRoot":"","sources":["../../../src/tools/reflect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,eAAO,MAAM,iBAAiB;;;;EAoCK,CAAC;AAEpC,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,OAAO,GACd,OAAO,CAAC,GAAG,CAAC,CAWd"}
1
+ {"version":3,"file":"reflect.d.ts","sourceRoot":"","sources":["../../../src/tools/reflect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,eAAO,MAAM,iBAAiB;;;;;EAwCK,CAAC;AASpC,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,OAAO,GACd,OAAO,CAAC,GAAG,CAAC,CA0Bd"}
@@ -1,31 +1,52 @@
1
- import { Type, MemoryTypeSchema } from "../types/typebox.js";
1
+ import { Type } from "../types/typebox.js";
2
2
  export const ReflectToolSchema = Type.Object({
3
3
  time_window: Type.Optional(Type.Union([
4
+ Type.Literal("recent"),
5
+ Type.Literal("today"),
6
+ Type.Literal("week"),
7
+ Type.Literal("month"),
4
8
  Type.Literal("1d"),
5
9
  Type.Literal("7d"),
6
10
  Type.Literal("30d"),
7
11
  Type.Literal("90d"),
8
12
  ], {
9
- description: "Time period to analyze. Defaults to last 7 days.",
10
- examples: ["7d", "30d"],
13
+ description: 'Time period to analyze: "recent" (default, no time cutoff), "today"/"1d", "week"/"7d", "month"/"30d", or "90d".',
14
+ examples: ["recent", "week", "30d"],
11
15
  })),
12
- focus_areas: Type.Optional(Type.Array(Type.Union([
13
- Type.Literal("memory_usage"),
14
- Type.Literal("relationships"),
15
- Type.Literal("importance"),
16
- Type.Literal("topics"),
17
- Type.Literal("patterns"),
18
- ]), {
19
- description: "Areas to focus analysis on. Omit for full analysis.",
20
- examples: [["memory_usage", "relationships"], ["topics"]],
16
+ start_date: Type.Optional(Type.String({
17
+ description: "Filter memories created on or after this date (ISO 8601, e.g. '2025-01-01'). Overrides time_window when set.",
18
+ examples: ["2025-01-01"],
21
19
  })),
22
- memory_types: Type.Optional(Type.Array(MemoryTypeSchema, {
23
- description: "Filter by memory types. Defaults to all types.",
24
- examples: [["fact", "insight"], ["identity_core", "personality_trait"]],
20
+ end_date: Type.Optional(Type.String({
21
+ description: "Filter memories created on or before this date (ISO 8601, e.g. '2025-01-31'). Overrides time_window when set.",
22
+ examples: ["2025-01-31"],
23
+ })),
24
+ include_documents: Type.Optional(Type.Boolean({
25
+ description: "Include document chunks in analysis. Default false (only user-created memories).",
25
26
  })),
26
27
  }, { additionalProperties: false });
28
+ /** Map shorthand aliases to API-native time_window values */
29
+ const TIME_WINDOW_ALIASES = {
30
+ "1d": "today",
31
+ "7d": "week",
32
+ "30d": "month",
33
+ };
27
34
  export async function executeReflectTool(apiClient, params) {
28
- const response = await apiClient.post("/api/v2/analysis/reflect", params);
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- params validated by schema
36
+ const p = { ...params };
37
+ if (p.time_window === "90d") {
38
+ // API has no 90d window — convert to explicit date range (matches MCP behavior)
39
+ if (!p.start_date) {
40
+ const d = new Date();
41
+ d.setDate(d.getDate() - 90);
42
+ p.start_date = d.toISOString().slice(0, 10);
43
+ }
44
+ p.time_window = "recent";
45
+ }
46
+ else if (p.time_window && p.time_window in TIME_WINDOW_ALIASES) {
47
+ p.time_window = TIME_WINDOW_ALIASES[p.time_window];
48
+ }
49
+ const response = await apiClient.post("/api/v2/analysis/reflect", p);
29
50
  return {
30
51
  content: [
31
52
  {
@@ -40,6 +40,16 @@ export interface OpenClawPluginCliContext {
40
40
  workspaceDir?: string;
41
41
  logger: PluginLogger;
42
42
  }
43
+ /** Event for before_agent_start hook */
44
+ export interface PluginHookBeforeAgentStartEvent {
45
+ prompt: string;
46
+ messages?: unknown[];
47
+ }
48
+ /** Result for before_agent_start hook — prependContext is injected before the agent prompt */
49
+ export interface PluginHookBeforeAgentStartResult {
50
+ systemPrompt?: string;
51
+ prependContext?: string;
52
+ }
43
53
  export interface OpenClawPluginApi {
44
54
  /** Plugin identifier */
45
55
  id: string;
@@ -77,6 +87,10 @@ export interface OpenClawPluginApi {
77
87
  }): void;
78
88
  /** Register service lifecycle */
79
89
  registerService(service: OpenClawPluginService): void;
90
+ /** Register a typed plugin lifecycle hook (always available, no config gate) */
91
+ on(hookName: string, handler: (...args: any[]) => any, opts?: {
92
+ priority?: number;
93
+ }): void;
80
94
  /** Runtime context */
81
95
  runtime?: {
82
96
  prompter?: {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,9 @@
6
6
  "additionalProperties": false,
7
7
  "properties": {
8
8
  "authUrl": { "type": "string" },
9
- "apiUrl": { "type": "string" }
9
+ "apiUrl": { "type": "string" },
10
+ "autoAwaken": { "type": "boolean", "default": true },
11
+ "autoOrient": { "type": "boolean", "default": true }
10
12
  }
11
13
  }
12
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-penfield",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Native OpenClaw plugin for Penfield memory integration",
6
6
  "main": "dist/index.js",
@@ -39,16 +39,15 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^25.0.9",
42
- "@typescript-eslint/eslint-plugin": "^7.0.0",
43
- "@typescript-eslint/parser": "^7.0.0",
44
- "eslint": "^8.56.0",
42
+ "eslint": "^9.39.2",
45
43
  "prettier": "^3.2.5",
46
- "typescript": "^5.3.3"
44
+ "typescript": "^5.3.3",
45
+ "typescript-eslint": "^8.54.0"
47
46
  },
48
47
  "scripts": {
49
48
  "build": "tsc",
50
- "lint": "eslint . --ext .ts",
51
- "lint:fix": "eslint . --ext .ts --fix",
49
+ "lint": "eslint .",
50
+ "lint:fix": "eslint . --fix",
52
51
  "format": "prettier --write \"**/*.{ts,json,md}\"",
53
52
  "format:check": "prettier --check \"**/*.{ts,json,md}\"",
54
53
  "typecheck": "tsc --noEmit"