@brianmichel/pi-noodle 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/LICENSE +21 -0
- package/README.md +231 -0
- package/index.ts +1 -0
- package/package.json +70 -0
- package/src/AGENTS.md +33 -0
- package/src/commands/index.ts +51 -0
- package/src/commands/memory-crud.ts +136 -0
- package/src/commands/review.ts +291 -0
- package/src/commands/setup.ts +189 -0
- package/src/commands/status.ts +32 -0
- package/src/commands/ui.ts +14 -0
- package/src/commands/web.ts +40 -0
- package/src/commands.ts +1 -0
- package/src/config/schema.ts +234 -0
- package/src/config-screen.ts +439 -0
- package/src/config.ts +159 -0
- package/src/constants.ts +1 -0
- package/src/debug-overlay.ts +230 -0
- package/src/extension.ts +166 -0
- package/src/index.ts +1 -0
- package/src/memory/backend.ts +22 -0
- package/src/memory/embedder.ts +7 -0
- package/src/memory/embedders/lm-studio.ts +25 -0
- package/src/memory/embedders/openai.ts +66 -0
- package/src/memory/extractor.ts +189 -0
- package/src/memory/policy.ts +325 -0
- package/src/memory/project-identity.ts +51 -0
- package/src/memory/runtime.ts +70 -0
- package/src/memory/service.ts +761 -0
- package/src/memory/turso-backend.ts +716 -0
- package/src/memory/types.ts +192 -0
- package/src/notifications.ts +11 -0
- package/src/queue.ts +42 -0
- package/src/session.ts +72 -0
- package/src/tools.ts +172 -0
- package/src/types.ts +81 -0
- package/src/utils.ts +68 -0
- package/src/web/dev.ts +7 -0
- package/src/web/index.html +1963 -0
- package/src/web/manager.ts +92 -0
- package/src/web/run.ts +33 -0
- package/src/web/server.ts +212 -0
- package/tsconfig.json +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brian Michel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# pi-noodle
|
|
2
|
+
|
|
3
|
+
Long-term memory for Pi.
|
|
4
|
+
|
|
5
|
+
This repo is building a small, opinionated memory system that tries to be:
|
|
6
|
+
|
|
7
|
+
- **useful** — retrieve facts that actually help future turns
|
|
8
|
+
- **safe** — avoid saving temporary or sensitive content
|
|
9
|
+
- **automatic** — capture durable signals from normal conversation
|
|
10
|
+
- **inspectable** — review what was saved, pending, or discarded
|
|
11
|
+
|
|
12
|
+
Under the hood it uses [libSQL](https://turso.tech/libsql) for storage and vector similarity search for retrieval.
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install as a Pi extension
|
|
20
|
+
pi install @brianmichel/pi-noodle
|
|
21
|
+
|
|
22
|
+
# In Pi, configure interactively
|
|
23
|
+
/noodle settings
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The setup screen shows the full config on one page:
|
|
27
|
+
1. Database mode — local file or Turso Cloud
|
|
28
|
+
2. Embedding provider — OpenAI, LM Studio, Ollama, or custom
|
|
29
|
+
3. Relevant fields update in place as you switch modes/providers
|
|
30
|
+
4. Required fields are validated before save
|
|
31
|
+
|
|
32
|
+
Settings are saved to `~/.pi/noodle/config.json` — memories travel with you across all projects.
|
|
33
|
+
|
|
34
|
+
## `/noodle` command
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
/noodle Show current config (paths, endpoint, masked API key)
|
|
38
|
+
/noodle remember <text> Save a memory directly
|
|
39
|
+
/noodle forget <query> Find and delete a memory
|
|
40
|
+
/noodle edit <query> Find and update a memory
|
|
41
|
+
/noodle review Review recent auto-saved memories
|
|
42
|
+
/noodle settings Interactive single-screen configuration editor with validation
|
|
43
|
+
/noodle setup Alias for /noodle settings
|
|
44
|
+
/noodle init Create a default config file for manual editing
|
|
45
|
+
/noodle web Start the Memory Explorer (auto-stops when all tabs close)
|
|
46
|
+
/noodle web stop Stop the explorer immediately
|
|
47
|
+
/noodle web dev Dev mode — hot reload on save, use web stop when done
|
|
48
|
+
/noodle web 8080 Start on a custom port
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For UI development outside Pi, run `npm run web:dev` from the repo — it connects to your configured database and reloads the browser whenever you edit `src/web/index.html`.
|
|
52
|
+
|
|
53
|
+
### Memory Explorer Web UI
|
|
54
|
+
|
|
55
|
+
Launch a dark-themed web interface to browse, search, and visualize your memories:
|
|
56
|
+
|
|
57
|
+
- **Live stats** — total memories, categories, scopes
|
|
58
|
+
- **Category filter** — dropdown of all stored categories
|
|
59
|
+
- **Text search** — substring matching on memory text
|
|
60
|
+
- **Dark mode** — GitHub-inspired color scheme
|
|
61
|
+
|
|
62
|
+
Run `/noodle web` in Pi to open the explorer in your browser. The server runs in a background process and **shuts down automatically ~2 seconds after you close all tabs**. Use `/noodle web stop` to kill it manually.
|
|
63
|
+
|
|
64
|
+
## Config file
|
|
65
|
+
|
|
66
|
+
`~/.pi/noodle/config.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"db": {
|
|
71
|
+
"mode": "local",
|
|
72
|
+
"path": "/Users/you/.pi/noodle/memories.db"
|
|
73
|
+
},
|
|
74
|
+
"embedding": {
|
|
75
|
+
"provider": "openai",
|
|
76
|
+
"apiKey": "sk-...",
|
|
77
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
78
|
+
"model": "text-embedding-3-small"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Cloud mode (Turso)
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"db": {
|
|
88
|
+
"mode": "cloud",
|
|
89
|
+
"url": "libsql://my-db-org.turso.io",
|
|
90
|
+
"authToken": "eyJ..."
|
|
91
|
+
},
|
|
92
|
+
"embedding": {
|
|
93
|
+
"provider": "openai",
|
|
94
|
+
"apiKey": "sk-...",
|
|
95
|
+
"baseUrl": "https://api.openai.com/v1",
|
|
96
|
+
"model": "text-embedding-3-small"
|
|
97
|
+
},
|
|
98
|
+
"extractor": {
|
|
99
|
+
"mode": "balanced",
|
|
100
|
+
"model": "deepseek/deepseek-v4-flash:free",
|
|
101
|
+
"triggerEvery": 10
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Memory modes
|
|
107
|
+
|
|
108
|
+
When memory mode is not `off`, capture uses a **unified capture pipeline** with policy-gated persistence:
|
|
109
|
+
|
|
110
|
+
- `conservative`
|
|
111
|
+
- fewer extraction runs
|
|
112
|
+
- higher bar for auto-save
|
|
113
|
+
- softer inferences are usually discarded until reinforced
|
|
114
|
+
- `balanced` (recommended default)
|
|
115
|
+
- durable facts can auto-save
|
|
116
|
+
- medium-confidence preferences go to `/noodle review`
|
|
117
|
+
- `proactive`
|
|
118
|
+
- more frequent extraction
|
|
119
|
+
- more candidate discovery
|
|
120
|
+
- pending review queue grows faster, but saved-memory safety rules stay the same
|
|
121
|
+
|
|
122
|
+
The extractor model is configurable too, so you can tune quality/speed/cost separately from behavior mode.
|
|
123
|
+
|
|
124
|
+
## Environment variable overrides
|
|
125
|
+
|
|
126
|
+
Env vars take priority over the config file:
|
|
127
|
+
|
|
128
|
+
| Variable | Overrides |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `NOODLE_CONFIG_PATH` | Config file location |
|
|
131
|
+
| `NOODLE_DB_PATH` | Local DB path |
|
|
132
|
+
| `NOODLE_DB_URL` | Cloud DB URL |
|
|
133
|
+
| `NOODLE_DB_TOKEN` | Cloud DB auth token |
|
|
134
|
+
| `OPENAI_API_KEY` | Embedding API key |
|
|
135
|
+
| `EMBEDDING_BASE_URL` | Embedding endpoint URL |
|
|
136
|
+
| `EMBEDDING_MODEL` | Embedding model name |
|
|
137
|
+
| `NOODLE_EXTRACTOR_MODE` | Memory mode: off / conservative / balanced / proactive |
|
|
138
|
+
| `NOODLE_EXTRACTOR_MODEL` | Extractor model ID |
|
|
139
|
+
| `NOODLE_EXTRACTOR_TRIGGER_EVERY` | Automatic extraction cadence in user turns |
|
|
140
|
+
| `NOODLE_EXTRACTOR_DEBUG` | Show the extractor debug widget: true / false |
|
|
141
|
+
|
|
142
|
+
## Architecture
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
Pi lifecycle events
|
|
146
|
+
└─► MemoryService.capture(event)
|
|
147
|
+
├─► heuristic capture
|
|
148
|
+
├─► optional LLM extraction
|
|
149
|
+
├─► candidate promotion policy
|
|
150
|
+
└─► conversation capture / consolidation when needed
|
|
151
|
+
│
|
|
152
|
+
▼
|
|
153
|
+
MemoryBackend
|
|
154
|
+
│
|
|
155
|
+
TursoBackend
|
|
156
|
+
├─► libSQL (local or cloud)
|
|
157
|
+
└─► Embedder
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Capture pipeline
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
input / compact / switch / shutdown
|
|
164
|
+
│
|
|
165
|
+
└─► MemoryService.capture(event)
|
|
166
|
+
│
|
|
167
|
+
├─► heuristic prefilter
|
|
168
|
+
│ ├─ blocks sensitive / temporary content
|
|
169
|
+
│ └─ catches explicit memory asks
|
|
170
|
+
│
|
|
171
|
+
├─► optional LLM extraction
|
|
172
|
+
│ └─ turns conversation context into memory candidates
|
|
173
|
+
│
|
|
174
|
+
├─► local promotion policy
|
|
175
|
+
│ ├─ save → durable memory DB
|
|
176
|
+
│ ├─ pending → /noodle review only
|
|
177
|
+
│ └─ discard → dropped
|
|
178
|
+
│
|
|
179
|
+
└─► retrieval injects only saved memories
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Why it is shaped this way
|
|
183
|
+
|
|
184
|
+
- Pi only tells the memory system **what event happened**
|
|
185
|
+
- `MemoryService` decides **which capture stages should run**
|
|
186
|
+
- heuristic and LLM candidates share the **same promotion path**
|
|
187
|
+
- pending memories stay out of retrieval until reviewed or reinforced
|
|
188
|
+
|
|
189
|
+
### What gets stored
|
|
190
|
+
|
|
191
|
+
Every memory is a row in SQLite with `text`, `embedding` (F32_BLOB), `category`, `categories`, `scope` (userId/assistantId/sessionId), and arbitrary `metadata`.
|
|
192
|
+
|
|
193
|
+
### Search
|
|
194
|
+
|
|
195
|
+
Vector similarity via `vector_distance_cos()` in libSQL, ranked by cosine distance, post-filtered by category and threshold.
|
|
196
|
+
|
|
197
|
+
### Policy and review
|
|
198
|
+
|
|
199
|
+
The system intentionally separates:
|
|
200
|
+
|
|
201
|
+
- **detection** — heuristics and LLM extraction find memory candidates
|
|
202
|
+
- **promotion** — local policy decides save / pending / discard
|
|
203
|
+
- **retrieval** — only saved memories are injected into prompts
|
|
204
|
+
|
|
205
|
+
That keeps the system proactive without letting low-confidence guesses pollute retrieval. Pending candidates stay visible in `/noodle review` but are **not** injected until promoted.
|
|
206
|
+
|
|
207
|
+
## File layout
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
src/
|
|
211
|
+
├── config.ts # Config resolution (~/.pi/noodle/config.json + env vars)
|
|
212
|
+
├── config-screen.ts # Flat single-screen config editor for /noodle settings
|
|
213
|
+
├── constants.ts # DEFAULT_AGENT_ID
|
|
214
|
+
├── types.ts # NoodleConfig, JsonObject, NotificationTarget, etc.
|
|
215
|
+
├── utils.ts # maskSecret, describeError, formatJson, extractTextContent
|
|
216
|
+
├── commands.ts # /noodle command + interactive setup entrypoint
|
|
217
|
+
├── extension.ts # Pi extension lifecycle hooks
|
|
218
|
+
├── tools.ts # memory_add / search / list / get / update / delete
|
|
219
|
+
├── session.ts # Session message collection
|
|
220
|
+
├── queue.ts # Sequential async write queue
|
|
221
|
+
├── notifications.ts # UI notification helpers
|
|
222
|
+
└── memory/
|
|
223
|
+
├── backend.ts # MemoryBackend interface
|
|
224
|
+
├── types.ts # MemoryRecord, MemoryScope, etc.
|
|
225
|
+
├── turso-backend.ts # TursoBackend (libSQL + vector search)
|
|
226
|
+
├── embedder.ts # Embedder type
|
|
227
|
+
├── embedders/ # openai.ts, lm-studio.ts
|
|
228
|
+
├── service.ts # MemoryService (event-driven capture pipeline + promotion)
|
|
229
|
+
├── policy.ts # Heuristics (classification, repetition, retrieval)
|
|
230
|
+
└── runtime.ts # Wiring (config + TursoBackend + MemoryService)
|
|
231
|
+
```
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./src/index.ts";
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brianmichel/pi-noodle",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Long-term memory for Pi — local libSQL database with vector search.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi",
|
|
8
|
+
"pi.dev",
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"pi-plugin",
|
|
12
|
+
"ai-agent",
|
|
13
|
+
"agent-memory",
|
|
14
|
+
"memory",
|
|
15
|
+
"libsql",
|
|
16
|
+
"turso",
|
|
17
|
+
"vector-search"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/brianmichel/pi-noodle.git"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/brianmichel/pi-noodle#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/brianmichel/pi-noodle/issues"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"index.ts",
|
|
36
|
+
"src",
|
|
37
|
+
"README.md",
|
|
38
|
+
"tsconfig.json"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "npm run check",
|
|
42
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
43
|
+
"clean": "echo 'nothing to clean'",
|
|
44
|
+
"test": "node --test --experimental-strip-types test/**/*.test.ts",
|
|
45
|
+
"web:dev": "bun run src/web/dev.ts"
|
|
46
|
+
},
|
|
47
|
+
"exports": {
|
|
48
|
+
".": "./index.ts"
|
|
49
|
+
},
|
|
50
|
+
"pi": {
|
|
51
|
+
"extensions": [
|
|
52
|
+
"./src/index.ts"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@libsql/client": "^0.15.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@earendil-works/pi-ai": "*",
|
|
60
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@earendil-works/pi-ai": "*",
|
|
64
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
65
|
+
"@earendil-works/pi-tui": "^0.75.5",
|
|
66
|
+
"@types/bun": "^1.2.0",
|
|
67
|
+
"@types/node": "^24.10.1",
|
|
68
|
+
"typescript": "^5.9.3"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/AGENTS.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file applies to this entire repository.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
We are trying to build a minimal, high-quality memory system for the Pi.dev agent harness. We aim to have a high quality test suite to help prove the efficacy of this project. It should be straight forward for memories to be stored, and retrieved. Ideally this happens automatically for the user.
|
|
8
|
+
|
|
9
|
+
## Type safety
|
|
10
|
+
|
|
11
|
+
Prefer creating types where needed to represent things instead of just strings. We like types since they help us better understand and reason about our code. Use the TypeScript type system to make contracts clear, and ensure that side effects are easily dealt with.
|
|
12
|
+
|
|
13
|
+
## Tooling
|
|
14
|
+
|
|
15
|
+
This project uses mise to provide a unified interface into dependencies and tasks. The following tasks are available:
|
|
16
|
+
|
|
17
|
+
- `mise check`: run the TypeScript type checker
|
|
18
|
+
- `mise install`: install any dependencies
|
|
19
|
+
- `mise precommit`: run precommit validation
|
|
20
|
+
- `mise test`: runs this project's unit test suite.
|
|
21
|
+
|
|
22
|
+
## Dead Code & Comments
|
|
23
|
+
|
|
24
|
+
- Delete dead code. Do not deprecate it, alias it, or leave it behind "for consumers." This is a private monorepo, not a published library.
|
|
25
|
+
- When a refactor replaces an interface or flow, remove the superseded entrypoints in the same change. Do not keep compatibility wrappers, transitional fallbacks, or duplicate code paths unless the user explicitly asks for a staged migration.
|
|
26
|
+
- Update tests and callers to the new seam instead of preserving the old one.
|
|
27
|
+
- Do not add decorative section-divider comments (e.g. `// -----------`).
|
|
28
|
+
- Do not add comments that restate what the code already says.
|
|
29
|
+
- JSDoc on public package exports is expected.
|
|
30
|
+
|
|
31
|
+
## Validation
|
|
32
|
+
|
|
33
|
+
You can validate your work by running the precommit task and ensure it has a normal exit status and there is no abnormal output.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { resolveConfigPath, writeConfig } from "../config.ts";
|
|
4
|
+
import { runEdit, runForget, runRemember } from "./memory-crud.ts";
|
|
5
|
+
import { runReview } from "./review.ts";
|
|
6
|
+
import { runSetup } from "./setup.ts";
|
|
7
|
+
import { runStatus } from "./status.ts";
|
|
8
|
+
import type { CtxUi } from "./ui.ts";
|
|
9
|
+
import { runWeb } from "./web.ts";
|
|
10
|
+
|
|
11
|
+
export function registerCommands(pi: ExtensionAPI): void {
|
|
12
|
+
pi.registerCommand("noodle", {
|
|
13
|
+
description: "Noodle memory — status, remember/forget/edit, review, and web explorer",
|
|
14
|
+
handler: async (args, ctx) => {
|
|
15
|
+
const sub = args.trim();
|
|
16
|
+
const ui = ctx.ui as unknown as CtxUi;
|
|
17
|
+
|
|
18
|
+
if (sub === "settings" || sub === "setup") {
|
|
19
|
+
await runSetup(ui);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (sub === "review") {
|
|
23
|
+
await runReview(ui);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (sub.startsWith("remember")) {
|
|
27
|
+
await runRemember(ui, sub.slice("remember".length).trim());
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (sub.startsWith("forget")) {
|
|
31
|
+
await runForget(ui, sub.slice("forget".length).trim());
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (sub.startsWith("edit")) {
|
|
35
|
+
await runEdit(ui, sub.slice("edit".length).trim());
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (sub === "init") {
|
|
39
|
+
writeConfig({});
|
|
40
|
+
ctx.ui.notify(`Created config at ${resolveConfigPath()}. Run /noodle settings to configure.`, "info");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (sub.startsWith("web")) {
|
|
44
|
+
await runWeb(ui, sub);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
runStatus(ui);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { memoryService } from "../memory/runtime.ts";
|
|
2
|
+
import type { MemoryRecord } from "../memory/types.ts";
|
|
3
|
+
import { describeError } from "../utils.ts";
|
|
4
|
+
import type { CtxUi } from "./ui.ts";
|
|
5
|
+
|
|
6
|
+
export async function runRemember(ui: CtxUi, initialText: string): Promise<void> {
|
|
7
|
+
try {
|
|
8
|
+
const text = (initialText || await ui.input("Memory to save", "") || "").trim();
|
|
9
|
+
if (!text) {
|
|
10
|
+
ui.notify("Nothing saved — memory text is required.", "info");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
await memoryService.add({
|
|
15
|
+
text,
|
|
16
|
+
metadata: {
|
|
17
|
+
source: "manual_command",
|
|
18
|
+
auto_saved: false,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
ui.notify(`Saved memory: ${summarizeMemory(text)}`, "info");
|
|
22
|
+
} catch (error) {
|
|
23
|
+
ui.notify(`Remember failed: ${describeError(error)}`, "error");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runForget(ui: CtxUi, queryText: string): Promise<void> {
|
|
28
|
+
try {
|
|
29
|
+
const query = (queryText || await ui.input("Find memory to forget", "") || "").trim();
|
|
30
|
+
if (!query) {
|
|
31
|
+
ui.notify("Forget cancelled — enter a memory query.", "info");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const target = await pickMemoryForAction(ui, query, "delete");
|
|
36
|
+
if (!target?.id) return;
|
|
37
|
+
|
|
38
|
+
const ok = await ui.confirm("Delete this memory?", target.text);
|
|
39
|
+
if (!ok) {
|
|
40
|
+
ui.notify("Forget cancelled.", "info");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await memoryService.delete(target.id);
|
|
45
|
+
ui.notify(`Deleted memory: ${summarizeMemory(target.text)}`, "info");
|
|
46
|
+
} catch (error) {
|
|
47
|
+
ui.notify(`Forget failed: ${describeError(error)}`, "error");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runEdit(ui: CtxUi, queryText: string): Promise<void> {
|
|
52
|
+
try {
|
|
53
|
+
const query = (queryText || await ui.input("Find memory to edit", "") || "").trim();
|
|
54
|
+
if (!query) {
|
|
55
|
+
ui.notify("Edit cancelled — enter a memory query.", "info");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const target = await pickMemoryForAction(ui, query, "edit");
|
|
60
|
+
if (!target?.id) return;
|
|
61
|
+
|
|
62
|
+
const replacement = (await ui.input("Replacement text", target.text) || "").trim();
|
|
63
|
+
if (!replacement) {
|
|
64
|
+
ui.notify("Edit cancelled — replacement text is required.", "info");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (replacement === target.text) {
|
|
68
|
+
ui.notify("No changes made.", "info");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await memoryService.update(target.id, {
|
|
73
|
+
text: replacement,
|
|
74
|
+
metadata: {
|
|
75
|
+
...target.metadata,
|
|
76
|
+
source: "manual_edit",
|
|
77
|
+
updated_from: target.text,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
ui.notify(`Updated memory: ${summarizeMemory(replacement)}`, "info");
|
|
81
|
+
} catch (error) {
|
|
82
|
+
ui.notify(`Edit failed: ${describeError(error)}`, "error");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function pickMemoryForAction(
|
|
87
|
+
ui: CtxUi,
|
|
88
|
+
query: string,
|
|
89
|
+
action: "edit" | "delete",
|
|
90
|
+
): Promise<MemoryRecord | null> {
|
|
91
|
+
const matches = await findMemoryMatches(query);
|
|
92
|
+
if (matches.length === 0) {
|
|
93
|
+
ui.notify(`No memories matched: ${query}`, "info");
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (matches.length === 1) {
|
|
97
|
+
return matches[0] ?? null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ui.notify(`Top matches for ${action}:`, "info");
|
|
101
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
102
|
+
ui.notify(`[${index + 1}] ${summarizeMemory(matches[index]!.text)}`, "info");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const raw = (await ui.input(`Choose memory to ${action} (1-${matches.length})`, "1") || "").trim();
|
|
106
|
+
const index = parseInt(raw, 10) - 1;
|
|
107
|
+
if (Number.isNaN(index) || index < 0 || index >= matches.length) {
|
|
108
|
+
ui.notify(`Invalid selection — cancelled ${action}.`, "info");
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return matches[index] ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function findMemoryMatches(query: string): Promise<MemoryRecord[]> {
|
|
116
|
+
const normalized = query.trim().toLowerCase();
|
|
117
|
+
if (!normalized) return [];
|
|
118
|
+
|
|
119
|
+
const semantic = await memoryService.search({ query, limit: 8 }).catch(() => []);
|
|
120
|
+
const listed = await memoryService.list();
|
|
121
|
+
const substring = listed.filter((memory) => memory.text.toLowerCase().includes(normalized));
|
|
122
|
+
|
|
123
|
+
const deduped = new Map<string, MemoryRecord>();
|
|
124
|
+
for (const memory of [...semantic, ...substring]) {
|
|
125
|
+
const key = memory.id ?? memory.text;
|
|
126
|
+
if (!deduped.has(key)) deduped.set(key, memory);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return Array.from(deduped.values()).slice(0, 8);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function summarizeMemory(text: string, max = 80): string {
|
|
133
|
+
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
134
|
+
if (singleLine.length <= max) return singleLine;
|
|
135
|
+
return `${singleLine.slice(0, Math.max(0, max - 1))}…`;
|
|
136
|
+
}
|