@parkgogogo/openclaw-reflection 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/INSTALL.md +78 -0
- package/README.md +195 -0
- package/openclaw.plugin.json +67 -0
- package/package.json +52 -0
- package/src/buffer.ts +40 -0
- package/src/config.ts +254 -0
- package/src/consolidation/consolidator.ts +316 -0
- package/src/consolidation/index.ts +9 -0
- package/src/consolidation/prompt.ts +58 -0
- package/src/consolidation/scheduler.ts +153 -0
- package/src/consolidation/types.ts +25 -0
- package/src/evals/cli.ts +45 -0
- package/src/evals/datasets.ts +39 -0
- package/src/evals/runner.ts +446 -0
- package/src/file-curator/index.ts +204 -0
- package/src/index.ts +323 -0
- package/src/llm/index.ts +11 -0
- package/src/llm/service.ts +447 -0
- package/src/llm/types.ts +87 -0
- package/src/logger.ts +125 -0
- package/src/memory-gate/analyzer.ts +191 -0
- package/src/memory-gate/index.ts +7 -0
- package/src/memory-gate/prompt.ts +85 -0
- package/src/memory-gate/types.ts +23 -0
- package/src/message-handler.ts +862 -0
- package/src/proper-lockfile.d.ts +25 -0
- package/src/session-manager.ts +114 -0
- package/src/types.ts +109 -0
- package/src/utils/file-utils.ts +228 -0
package/INSTALL.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# OpenClaw Install Guide
|
|
2
|
+
|
|
3
|
+
This file is for OpenClaw itself to follow when installing `@parkgogogo/openclaw-reflection`.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Install the packaged plugin, enable it under the `openclaw-reflection` plugin id, provide a valid LLM config, and restart the gateway so the plugin becomes active.
|
|
8
|
+
|
|
9
|
+
## Install From npm
|
|
10
|
+
|
|
11
|
+
Use the published package, not a linked workspace checkout:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
openclaw plugins install @parkgogogo/openclaw-reflection@0.1.0
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If you are installing from a local release artifact instead:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
openclaw plugins install ./parkgogogo-openclaw-reflection-0.1.0.tgz
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Write Config
|
|
24
|
+
|
|
25
|
+
Add this under `plugins.entries.openclaw-reflection` in the active OpenClaw profile config:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"enabled": true,
|
|
30
|
+
"config": {
|
|
31
|
+
"workspaceDir": "/absolute/path/to/agent-workspace",
|
|
32
|
+
"bufferSize": 50,
|
|
33
|
+
"logLevel": "info",
|
|
34
|
+
"llm": {
|
|
35
|
+
"baseURL": "https://api.openai.com/v1",
|
|
36
|
+
"apiKey": "YOUR_API_KEY",
|
|
37
|
+
"model": "gpt-4.1-mini"
|
|
38
|
+
},
|
|
39
|
+
"memoryGate": {
|
|
40
|
+
"enabled": true,
|
|
41
|
+
"windowSize": 10
|
|
42
|
+
},
|
|
43
|
+
"consolidation": {
|
|
44
|
+
"enabled": true,
|
|
45
|
+
"schedule": "0 2 * * *"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Make sure `plugins.allow` includes `openclaw-reflection` if the profile uses an allowlist.
|
|
52
|
+
|
|
53
|
+
## Validate And Restart
|
|
54
|
+
|
|
55
|
+
Run:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
openclaw config validate --json
|
|
59
|
+
openclaw gateway run --force --verbose
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
In another shell, confirm the gateway is live:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
curl --fail http://127.0.0.1:PORT/health
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Expected Success Signals
|
|
69
|
+
|
|
70
|
+
- The gateway starts without plugin registration errors
|
|
71
|
+
- The plugin log contains `Plugin registered successfully, all hooks active`
|
|
72
|
+
- `GET /health` returns JSON containing `"ok": true`
|
|
73
|
+
|
|
74
|
+
## Do Not Do This
|
|
75
|
+
|
|
76
|
+
- Do not use `openclaw plugins install -l` unless you are actively developing this repository
|
|
77
|
+
- Do not leave `llm.baseURL`, `llm.apiKey`, or `llm.model` empty
|
|
78
|
+
- Do not configure the plugin under the old id `reflection-plugin`
|
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# OpenClaw Reflection
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
**Make OpenClaw's native memory system sharper without replacing it.**
|
|
9
|
+
|
|
10
|
+
OpenClaw Reflection is an additive layer on top of OpenClaw's built-in Markdown memory system. It captures message flow, keeps thread noise out of long-term memory, writes durable knowledge into the same human-readable memory files OpenClaw already uses, and periodically consolidates them so your agent gets sharper over time instead of messier.
|
|
11
|
+
|
|
12
|
+
## Built On OpenClaw Memory
|
|
13
|
+
|
|
14
|
+
OpenClaw memory is already workspace-native: the source of truth is Markdown files in the agent workspace, not a hidden database. In the official model, daily logs live under `memory/YYYY-MM-DD.md`, while `MEMORY.md` is the curated long-term layer.
|
|
15
|
+
|
|
16
|
+
Reflection builds on top of that system instead of replacing it.
|
|
17
|
+
|
|
18
|
+
- It does **not** introduce a separate memory store
|
|
19
|
+
- It does **not** require replacing OpenClaw's default `memory-core`
|
|
20
|
+
- It does **not** take over the active `plugins.slots.memory` role
|
|
21
|
+
- It works by listening to message hooks and curating the same workspace memory files
|
|
22
|
+
|
|
23
|
+
In practice, that means low migration risk and low conceptual overhead: you keep OpenClaw's native MEMORY workflow, and Reflection enhances the capture, filtering, routing, and consolidation steps around it.
|
|
24
|
+
|
|
25
|
+
## Why People Install It
|
|
26
|
+
|
|
27
|
+
Most chat memory systems fail in one of two ways:
|
|
28
|
+
|
|
29
|
+
- they forget too much, so you keep re-explaining the same context
|
|
30
|
+
- they remember too much, so temporary thread noise pollutes long-term memory
|
|
31
|
+
|
|
32
|
+
Reflection is built to fix both.
|
|
33
|
+
|
|
34
|
+
- Keep stable user preferences and collaboration habits
|
|
35
|
+
- Preserve durable shared context across sessions
|
|
36
|
+
- Separate memory into `MEMORY.md`, `USER.md`, `SOUL.md`, `IDENTITY.md`, and `TOOLS.md`
|
|
37
|
+
- Refuse one-off tasks, active thread chatter, and misrouted writes
|
|
38
|
+
- Periodically consolidate memory so it stays usable
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
### Recommended for users: install the plugin package
|
|
43
|
+
|
|
44
|
+
OpenClaw can install plugins directly from a package source. That is the right distribution path for Reflection, because users should not need to clone the repository or run `pnpm install` just to use the plugin.
|
|
45
|
+
|
|
46
|
+
For a step-by-step installation flow that OpenClaw can follow directly, see [INSTALL.md](./INSTALL.md).
|
|
47
|
+
|
|
48
|
+
Registry install after publishing:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
openclaw plugins install <npm-spec>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
openclaw plugins install @parkgogogo/openclaw-reflection
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Add the plugin config
|
|
61
|
+
|
|
62
|
+
Put the following under `plugins.entries.openclaw-reflection` in your OpenClaw config:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"enabled": true,
|
|
67
|
+
"config": {
|
|
68
|
+
"workspaceDir": "/absolute/path/to/your-agent-workspace",
|
|
69
|
+
"bufferSize": 50,
|
|
70
|
+
"logLevel": "info",
|
|
71
|
+
"llm": {
|
|
72
|
+
"baseURL": "https://api.openai.com/v1",
|
|
73
|
+
"apiKey": "YOUR_API_KEY",
|
|
74
|
+
"model": "gpt-4.1-mini"
|
|
75
|
+
},
|
|
76
|
+
"memoryGate": {
|
|
77
|
+
"enabled": true,
|
|
78
|
+
"windowSize": 10
|
|
79
|
+
},
|
|
80
|
+
"consolidation": {
|
|
81
|
+
"enabled": true,
|
|
82
|
+
"schedule": "0 2 * * *"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Restart OpenClaw Gateway
|
|
89
|
+
|
|
90
|
+
Once the gateway restarts, Reflection will begin listening to `message_received` and `before_message_write`, then writing curated memory files into your configured `workspaceDir`.
|
|
91
|
+
|
|
92
|
+
## What You Get
|
|
93
|
+
|
|
94
|
+
| You want | Reflection gives you |
|
|
95
|
+
| ------------------------------------ | ---------------------------------------------------------- |
|
|
96
|
+
| A memory system you can inspect | Plain Markdown files you can open, edit, diff, and version |
|
|
97
|
+
| Better continuity across sessions | Durable facts routed into the right long-term file |
|
|
98
|
+
| Less memory pollution | Gatekeeping that refuses temporary or misrouted content |
|
|
99
|
+
| A system that stays usable over time | Scheduled consolidation for existing memory files |
|
|
100
|
+
|
|
101
|
+
## Why This Beats Naive Memory
|
|
102
|
+
|
|
103
|
+
| Naive memory | Reflection |
|
|
104
|
+
| -------------------------------- | ------------------------------------------------ |
|
|
105
|
+
| Appends whatever seems memorable | Filters for durable signal before writing |
|
|
106
|
+
| Hides memory in a black box | Stores memory in readable Markdown files |
|
|
107
|
+
| Mixes all facts together | Routes facts into purpose-specific files |
|
|
108
|
+
| Lets bad writes accumulate | Adds writer guarding and scheduled consolidation |
|
|
109
|
+
|
|
110
|
+
## How It Works
|
|
111
|
+
|
|
112
|
+
```mermaid
|
|
113
|
+
flowchart LR
|
|
114
|
+
A["Incoming conversation"] --> B["Session buffer"]
|
|
115
|
+
B --> C["memoryGate"]
|
|
116
|
+
C -->|durable fact| D["Writer guardian"]
|
|
117
|
+
C -->|thread noise| E["No write"]
|
|
118
|
+
D --> F["MEMORY.md / USER.md / SOUL.md / IDENTITY.md / TOOLS.md"]
|
|
119
|
+
F --> G["Scheduled consolidation"]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
In practice, the pipeline is simple:
|
|
123
|
+
|
|
124
|
+
1. Reflection captures conversation context from OpenClaw hooks.
|
|
125
|
+
2. `memoryGate` decides whether the candidate fact is durable enough to keep.
|
|
126
|
+
3. A file-specific guardian either rewrites the target memory file or refuses the write.
|
|
127
|
+
4. Scheduled consolidation keeps `MEMORY.md`, `USER.md`, `SOUL.md`, and `TOOLS.md` compact over time.
|
|
128
|
+
|
|
129
|
+
## Proof, Not Just Promises
|
|
130
|
+
|
|
131
|
+
This repo already includes offline eval coverage for the two hardest parts of the system:
|
|
132
|
+
|
|
133
|
+
- [`memoryGate`: 16/16 passed on V2](./evals/results/2026-03-08-memory-gate-v2-16-of-16.md)
|
|
134
|
+
- [`writer guardian`: 16/16 passed on V2](./evals/results/2026-03-08-writer-guardian-v2-16-of-16.md)
|
|
135
|
+
|
|
136
|
+
These evals focus on the failure modes that make long-term memory systems unreliable:
|
|
137
|
+
|
|
138
|
+
- refusing active thread noise
|
|
139
|
+
- keeping user facts out of the wrong file
|
|
140
|
+
- preserving `SOUL` continuity rules
|
|
141
|
+
- replacing outdated `IDENTITY` metadata correctly
|
|
142
|
+
- keeping local tool mappings in `TOOLS.md` without turning it into a tool registry
|
|
143
|
+
|
|
144
|
+
## The Memory Files
|
|
145
|
+
|
|
146
|
+
| File | Purpose |
|
|
147
|
+
| ------------- | ----------------------------------------------------------------------------------- |
|
|
148
|
+
| `MEMORY.md` | Durable shared context, important conclusions, long-lived background facts |
|
|
149
|
+
| `USER.md` | Stable user preferences, collaboration style, helpful personal context |
|
|
150
|
+
| `SOUL.md` | Assistant principles, boundaries, and continuity rules |
|
|
151
|
+
| `IDENTITY.md` | Explicit identity metadata such as name, vibe, or avatar-style descriptors |
|
|
152
|
+
| `TOOLS.md` | Environment-specific tool aliases, endpoints, device names, and local tool mappings |
|
|
153
|
+
|
|
154
|
+
## Advanced Configuration
|
|
155
|
+
|
|
156
|
+
| Key | Default | Meaning |
|
|
157
|
+
| ------------------------ | --------------------------- | ----------------------------------------- |
|
|
158
|
+
| `workspaceDir` | none | Directory where memory files are written |
|
|
159
|
+
| `bufferSize` | `50` | Session buffer size |
|
|
160
|
+
| `logLevel` | `info` | `debug`, `info`, `warn`, or `error` |
|
|
161
|
+
| `llm.baseURL` | `https://api.openai.com/v1` | OpenAI-compatible provider URL |
|
|
162
|
+
| `llm.apiKey` | empty | Provider API key |
|
|
163
|
+
| `llm.model` | `gpt-4.1-mini` | Model used for analysis and consolidation |
|
|
164
|
+
| `memoryGate.enabled` | `true` | Enable long-term memory filtering |
|
|
165
|
+
| `memoryGate.windowSize` | `10` | Message window used during analysis |
|
|
166
|
+
| `consolidation.enabled` | `true` | Enable scheduled consolidation |
|
|
167
|
+
| `consolidation.schedule` | `0 2 * * *` | Cron expression for consolidation |
|
|
168
|
+
|
|
169
|
+
## Built For
|
|
170
|
+
|
|
171
|
+
- personal agents that should get better over weeks, not just one session
|
|
172
|
+
- teams that want memory with reviewability and version control
|
|
173
|
+
- OpenClaw users who do not want a black-box memory store
|
|
174
|
+
- agents that need stronger continuity without turning every chat into permanent history
|
|
175
|
+
|
|
176
|
+
## Development And Evals
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pnpm run typecheck
|
|
180
|
+
pnpm run eval:memory-gate
|
|
181
|
+
pnpm run eval:writer-guardian
|
|
182
|
+
pnpm run eval:all
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
More eval details: [evals/README.md](./evals/README.md)
|
|
186
|
+
|
|
187
|
+
Fast packaged-plugin regression on a reused local OpenClaw profile:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
pnpm run e2e:openclaw-plugin
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Links
|
|
194
|
+
|
|
195
|
+
- OpenClaw plugin docs: [docs.openclaw.ai/tools/plugin](https://docs.openclaw.ai/tools/plugin)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "openclaw-reflection",
|
|
3
|
+
"entry": "src/index.ts",
|
|
4
|
+
"configSchema": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"workspaceDir": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "Directory used for USER.md, MEMORY.md, SOUL.md, and IDENTITY.md writes"
|
|
10
|
+
},
|
|
11
|
+
"bufferSize": {
|
|
12
|
+
"type": "integer",
|
|
13
|
+
"minimum": 1,
|
|
14
|
+
"default": 50
|
|
15
|
+
},
|
|
16
|
+
"logLevel": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"enum": ["debug", "info", "warn", "error"],
|
|
19
|
+
"default": "info"
|
|
20
|
+
},
|
|
21
|
+
"llm": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"baseURL": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"default": "https://api.openai.com/v1"
|
|
27
|
+
},
|
|
28
|
+
"apiKey": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"default": ""
|
|
31
|
+
},
|
|
32
|
+
"model": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"default": "gpt-4.1-mini"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"memoryGate": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"enabled": {
|
|
42
|
+
"type": "boolean",
|
|
43
|
+
"default": true
|
|
44
|
+
},
|
|
45
|
+
"windowSize": {
|
|
46
|
+
"type": "integer",
|
|
47
|
+
"minimum": 1,
|
|
48
|
+
"default": 10
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"consolidation": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"enabled": {
|
|
56
|
+
"type": "boolean",
|
|
57
|
+
"default": true
|
|
58
|
+
},
|
|
59
|
+
"schedule": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"default": "0 2 * * *"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@parkgogogo/openclaw-reflection",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenClaw plugin that enhances native Markdown memory with filtering, curation, and consolidation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src/",
|
|
9
|
+
"openclaw.plugin.json",
|
|
10
|
+
"README.md",
|
|
11
|
+
"INSTALL.md"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/parkgogogo/openclaw-reflection.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/parkgogogo/openclaw-reflection#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/parkgogogo/openclaw-reflection/issues"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc --noEmit",
|
|
23
|
+
"clean": "rm -rf logs",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"e2e:openclaw-plugin": "bash scripts/e2e-openclaw-plugin.sh",
|
|
26
|
+
"eval:memory-gate": "pnpm exec tsc && node evals/run.mjs --suite memory-gate",
|
|
27
|
+
"eval:writer-guardian": "pnpm exec tsc && node evals/run.mjs --suite writer-guardian",
|
|
28
|
+
"eval:all": "pnpm exec tsc && node evals/run.mjs --suite all"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"openclaw",
|
|
32
|
+
"plugin",
|
|
33
|
+
"reflection",
|
|
34
|
+
"message-buffer"
|
|
35
|
+
],
|
|
36
|
+
"author": "",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"openclaw": {
|
|
39
|
+
"extensions": [
|
|
40
|
+
"./src/index.ts"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"proper-lockfile": "^4.1.2",
|
|
45
|
+
"ulid": "^2.3.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^20.0.0",
|
|
49
|
+
"typescript": "^5.0.0"
|
|
50
|
+
},
|
|
51
|
+
"packageManager": "pnpm@8.6.9+sha512.2cf11a086be557875519e25e1ea8bfa4247f4844e8a2a99272fdb072bd204ea19479ba64b75c3d4c8117d1ac0b3212cedbda7ba5c5dfcf964ee7d07bd139dcd3"
|
|
52
|
+
}
|
package/src/buffer.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class CircularBuffer<T> {
|
|
2
|
+
private buffer: T[];
|
|
3
|
+
private capacity: number;
|
|
4
|
+
|
|
5
|
+
constructor(capacity: number) {
|
|
6
|
+
if (!Number.isInteger(capacity) || capacity <= 0) {
|
|
7
|
+
throw new Error("CircularBuffer capacity must be a positive integer");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
this.capacity = capacity;
|
|
11
|
+
this.buffer = [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
push(item: T): T | null {
|
|
15
|
+
let evicted: T | null = null;
|
|
16
|
+
|
|
17
|
+
if (this.isFull()) {
|
|
18
|
+
evicted = this.buffer.shift() ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.buffer.push(item);
|
|
22
|
+
return evicted;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
toArray(): T[] {
|
|
26
|
+
return [...this.buffer];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
isFull(): boolean {
|
|
30
|
+
return this.buffer.length >= this.capacity;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
size(): number {
|
|
34
|
+
return this.buffer.length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
clear(): void {
|
|
38
|
+
this.buffer = [];
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { LogLevel, PluginConfig } from "./types.js";
|
|
3
|
+
|
|
4
|
+
interface PluginAPI {
|
|
5
|
+
pluginConfig?: unknown;
|
|
6
|
+
config?: {
|
|
7
|
+
get?: (key: string) => unknown;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CONFIG: PluginConfig = {
|
|
12
|
+
bufferSize: 50,
|
|
13
|
+
logLevel: "info",
|
|
14
|
+
llm: {
|
|
15
|
+
baseURL: "https://api.openai.com/v1",
|
|
16
|
+
apiKey: "",
|
|
17
|
+
model: "gpt-4.1-mini",
|
|
18
|
+
},
|
|
19
|
+
memoryGate: {
|
|
20
|
+
enabled: true,
|
|
21
|
+
windowSize: 10,
|
|
22
|
+
},
|
|
23
|
+
consolidation: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
schedule: "0 2 * * *",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const VALID_LOG_LEVELS = new Set<LogLevel>(["debug", "info", "warn", "error"]);
|
|
30
|
+
|
|
31
|
+
function getPositiveInteger(value: unknown, fallback: number): number {
|
|
32
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getBoolean(value: unknown, fallback: boolean): boolean {
|
|
40
|
+
if (typeof value !== "boolean") {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getString(value: unknown, fallback: string): string {
|
|
48
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
56
|
+
return typeof value === "object" && value !== null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getLogLevel(value: unknown): LogLevel {
|
|
60
|
+
if (typeof value === "string" && VALID_LOG_LEVELS.has(value as LogLevel)) {
|
|
61
|
+
return value as LogLevel;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return DEFAULT_CONFIG.logLevel;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readConfigValue(
|
|
68
|
+
config: PluginAPI["config"],
|
|
69
|
+
key: string
|
|
70
|
+
): unknown {
|
|
71
|
+
const directValue = config?.get?.(key);
|
|
72
|
+
if (directValue !== undefined) {
|
|
73
|
+
return directValue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const segments = key.split(".");
|
|
77
|
+
if (segments.length === 1) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const [rootKey, ...nestedSegments] = segments;
|
|
82
|
+
let currentValue = config?.get?.(rootKey);
|
|
83
|
+
|
|
84
|
+
for (const segment of nestedSegments) {
|
|
85
|
+
if (!isRecord(currentValue) || !(segment in currentValue)) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
currentValue = currentValue[segment];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return currentValue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readRecordValue(
|
|
96
|
+
value: unknown,
|
|
97
|
+
key: string
|
|
98
|
+
): unknown {
|
|
99
|
+
const segments = key.split(".");
|
|
100
|
+
let currentValue: unknown = value;
|
|
101
|
+
|
|
102
|
+
for (const segment of segments) {
|
|
103
|
+
if (!isRecord(currentValue) || !(segment in currentValue)) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
currentValue = currentValue[segment];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return currentValue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readPluginConfigValue(api: PluginAPI, key: string): unknown {
|
|
114
|
+
const pluginConfigValue = readRecordValue(api.pluginConfig, key);
|
|
115
|
+
if (pluginConfigValue !== undefined) {
|
|
116
|
+
return pluginConfigValue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return readConfigValue(api.config, key);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type ConfigLogSnapshot = Record<string, unknown> & {
|
|
123
|
+
bufferSize: number;
|
|
124
|
+
logLevel: LogLevel;
|
|
125
|
+
llm: {
|
|
126
|
+
baseURL: string;
|
|
127
|
+
apiKeyConfigured: boolean;
|
|
128
|
+
model: string;
|
|
129
|
+
};
|
|
130
|
+
memoryGate: {
|
|
131
|
+
enabled: boolean;
|
|
132
|
+
windowSize: number;
|
|
133
|
+
};
|
|
134
|
+
consolidation: {
|
|
135
|
+
enabled: boolean;
|
|
136
|
+
schedule: string;
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export interface WorkspaceResolution {
|
|
141
|
+
workspaceDir?: string;
|
|
142
|
+
source: string;
|
|
143
|
+
reason?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getNonEmptyString(value: unknown): string | undefined {
|
|
147
|
+
if (typeof value !== "string") {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const trimmed = value.trim();
|
|
152
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isFilesystemRoot(dirPath: string): boolean {
|
|
156
|
+
const resolved = path.resolve(dirPath);
|
|
157
|
+
return resolved === path.parse(resolved).root;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function createConfigLogSnapshot(
|
|
161
|
+
config: PluginConfig
|
|
162
|
+
): ConfigLogSnapshot {
|
|
163
|
+
return {
|
|
164
|
+
bufferSize: config.bufferSize,
|
|
165
|
+
logLevel: config.logLevel,
|
|
166
|
+
llm: {
|
|
167
|
+
baseURL: config.llm.baseURL,
|
|
168
|
+
apiKeyConfigured: config.llm.apiKey.trim().length > 0,
|
|
169
|
+
model: config.llm.model,
|
|
170
|
+
},
|
|
171
|
+
memoryGate: {
|
|
172
|
+
enabled: config.memoryGate.enabled,
|
|
173
|
+
windowSize: config.memoryGate.windowSize,
|
|
174
|
+
},
|
|
175
|
+
consolidation: {
|
|
176
|
+
enabled: config.consolidation.enabled,
|
|
177
|
+
schedule: config.consolidation.schedule,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function resolveWorkspaceDir(
|
|
183
|
+
api: PluginAPI,
|
|
184
|
+
cwd: string = process.cwd()
|
|
185
|
+
): WorkspaceResolution {
|
|
186
|
+
const configuredWorkspaceDir = getNonEmptyString(
|
|
187
|
+
readPluginConfigValue(api, "workspaceDir")
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (configuredWorkspaceDir) {
|
|
191
|
+
return {
|
|
192
|
+
workspaceDir: path.resolve(configuredWorkspaceDir),
|
|
193
|
+
source: "plugin config workspaceDir",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const normalizedCwd = getNonEmptyString(cwd);
|
|
198
|
+
if (normalizedCwd && !isFilesystemRoot(normalizedCwd)) {
|
|
199
|
+
return {
|
|
200
|
+
workspaceDir: path.resolve(normalizedCwd),
|
|
201
|
+
source: "process.cwd() fallback",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
source: "unresolved",
|
|
207
|
+
reason:
|
|
208
|
+
'Missing plugin config "workspaceDir" and process.cwd() resolved to the filesystem root',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function parseConfig(api: PluginAPI): PluginConfig {
|
|
213
|
+
return {
|
|
214
|
+
bufferSize: getPositiveInteger(
|
|
215
|
+
readPluginConfigValue(api, "bufferSize"),
|
|
216
|
+
DEFAULT_CONFIG.bufferSize
|
|
217
|
+
),
|
|
218
|
+
logLevel: getLogLevel(readPluginConfigValue(api, "logLevel")),
|
|
219
|
+
llm: {
|
|
220
|
+
baseURL: getString(
|
|
221
|
+
readPluginConfigValue(api, "llm.baseURL"),
|
|
222
|
+
DEFAULT_CONFIG.llm.baseURL
|
|
223
|
+
),
|
|
224
|
+
apiKey: getString(
|
|
225
|
+
readPluginConfigValue(api, "llm.apiKey"),
|
|
226
|
+
DEFAULT_CONFIG.llm.apiKey
|
|
227
|
+
),
|
|
228
|
+
model: getString(
|
|
229
|
+
readPluginConfigValue(api, "llm.model"),
|
|
230
|
+
DEFAULT_CONFIG.llm.model
|
|
231
|
+
),
|
|
232
|
+
},
|
|
233
|
+
memoryGate: {
|
|
234
|
+
enabled: getBoolean(
|
|
235
|
+
readPluginConfigValue(api, "memoryGate.enabled"),
|
|
236
|
+
DEFAULT_CONFIG.memoryGate.enabled
|
|
237
|
+
),
|
|
238
|
+
windowSize: getPositiveInteger(
|
|
239
|
+
readPluginConfigValue(api, "memoryGate.windowSize"),
|
|
240
|
+
DEFAULT_CONFIG.memoryGate.windowSize
|
|
241
|
+
),
|
|
242
|
+
},
|
|
243
|
+
consolidation: {
|
|
244
|
+
enabled: getBoolean(
|
|
245
|
+
readPluginConfigValue(api, "consolidation.enabled"),
|
|
246
|
+
DEFAULT_CONFIG.consolidation.enabled
|
|
247
|
+
),
|
|
248
|
+
schedule: getString(
|
|
249
|
+
readPluginConfigValue(api, "consolidation.schedule"),
|
|
250
|
+
DEFAULT_CONFIG.consolidation.schedule
|
|
251
|
+
),
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|