@inetafrica/open-claudia 2.5.0 → 2.6.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/CHANGELOG.md +10 -0
- package/README.md +217 -120
- package/bin/cli.js +6 -0
- package/bin/dream.js +51 -0
- package/bot.js +2 -0
- package/core/dream.js +332 -0
- package/core/pack-review.js +31 -6
- package/core/persona.js +51 -0
- package/core/runner.js +4 -4
- package/core/system-prompt.js +25 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.6.0
|
|
4
|
+
- **Dream: nightly memory consolidation (Phase 3 of the memory system).** A quiet-hours pass (`core/dream.js`, default 4am via `DREAM_CRON`, model `DREAM_MODEL` default sonnet, `DREAM=off` to disable) reviews the entire pack + entity memory on a stronger model and returns a strict-JSON decision that bot code applies:
|
|
5
|
+
- **Merges** packs that drifted into the same topic (sections synthesised, not concatenated) — merged-away packs are backed up to `~/.open-claudia/backup/dream-<stamp>/` before removal.
|
|
6
|
+
- **Umbrella/parent pack trees**: creates umbrella packs over 3+ sibling topics (the umbrella's State is a family map, a router not a duplicate) and assigns parent/sub relationships, with cycle protection. Injected packs now show their parent ("Part of: …") and list sub-packs with dig-deeper hints.
|
|
7
|
+
- **Retags**: tightens pack descriptions and tags — the FTS router matches on those fields, so tighter metadata means fewer false injections.
|
|
8
|
+
- **Entity dedupe + cross-linking**: merges duplicate entities (aliases unioned, source backed up) and rewrites entity Notes with `[[pack-dir]]` cross-links.
|
|
9
|
+
- Reports in chat (owner Telegram channel) whenever it changed something; silent when there was nothing to tidy. Manual trigger: `open-claudia dream`, preview with `--dry-run`.
|
|
10
|
+
- **Personality system.** `~/.open-claudia/persona.md` (override with `PERSONA_FILE`) holds Open Claudia's voice — tone, quirks, emoji habits — on top of the user-owned soul. It feeds into the system prompt as a `## Personality` block that explicitly never overrides soul rules, ships with a warm default, and the dream pass may evolve it gently (bounded 80–2400 chars, previous version backed up, change announced).
|
|
11
|
+
- **Memory announcements got a face-lift.** Reviewer announcements are now grouped under a friendly header ("🧠 Took some notes while we worked:"), use per-kind emojis (📦 new pack, ✏️ update, 👤/🚀/🏢/📍/🖥️/🔖 entities by type), clip at word boundaries instead of mid-word, carry more of the actual content, and include an inspect hint for new packs. In-turn pack/entity writes announce in the same voice.
|
|
12
|
+
|
|
3
13
|
## v2.5.0
|
|
4
14
|
- **Context packs: skills become living topic documents (skills + memory merged).** Each pack is `~/.open-claudia/packs/<dir>/PACK.md` with four sections — Stance (how to think about the topic: user preferences, hard rules), Procedure (verified how-to, what skills used to be), State (where work stands now; replaced wholesale as things move), Journal (dated one-line log of past sessions, capped at 30 entries).
|
|
5
15
|
- **Pre-turn router** (`core/packs.js` + `system-prompt.js`): the incoming message is FTS5-matched against all packs (porter-stemmed, field-weighted — name/description/tags hits count double, threshold keeps a single incidental body word from dragging a pack in; BM25 was unusable because its IDF collapses on small corpora). Matched packs are injected into the per-turn context block, once per (channel, session, pack-version) so re-injection only happens when a pack actually changed. Rides the user message, not the system prompt, to preserve the prompt-cache prefix.
|
package/README.md
CHANGED
|
@@ -2,36 +2,58 @@
|
|
|
2
2
|
|
|
3
3
|
Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat.
|
|
4
4
|
|
|
5
|
-
Send text, voice notes, screenshots, and files from your phone. Your chosen AI agent works on your projects and reports back.
|
|
5
|
+
Send text, voice notes, screenshots, and files from your phone. Your chosen AI agent works on your projects, remembers what it learned, and reports back.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
+
### Channels & backends
|
|
9
10
|
- **Multi-channel** — run the same bot on Telegram, Kazee Chat, or both at once (`CHANNELS=telegram,kazee`). Each channel renders keyboards, files, voice notes, and edits natively
|
|
10
|
-
- **Multi-backend** — switch between Claude Code, Cursor Agent, and OpenAI Codex on the fly (`/claude`, `/cursor`, `/codex`)
|
|
11
|
-
- **Multi-
|
|
12
|
-
- **
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- **
|
|
11
|
+
- **Multi-backend** — switch between Claude Code, Cursor Agent, and OpenAI Codex on the fly (`/claude`, `/cursor`, `/codex`); each keeps its own session state
|
|
12
|
+
- **Multi-user / team mode** — one bot serves multiple authorized users in parallel, each with their own conversations, settings, model, backend, and usage counters
|
|
13
|
+
- **Multi-project sessions** — switch between workspace projects; per-project conversation history auto-resumes
|
|
14
|
+
|
|
15
|
+
### Memory & long-term context
|
|
16
|
+
- **Context packs** — living per-topic documents (one per project, system, or recurring task) holding Stance, Procedure, State, and Journal. Packs matching your message are auto-injected into the agent's context, and a background reviewer updates them after every substantial turn — the assistant keeps its train of thought across sessions and projects
|
|
17
|
+
- **Entity memory** — short notes on the people, places, projects, orgs, and systems you mention, extracted automatically and injected when they come up again
|
|
18
|
+
- **Dream consolidation** — a nightly pass on a stronger model that merges duplicate packs, builds umbrella/parent pack trees, tightens descriptions, dedupes entities, and reports what it tidied — with everything backed up first
|
|
19
|
+
- **Personality** — a persona file gives the assistant a consistent voice on top of your soul file, and the dream pass evolves it gently as you work together
|
|
20
|
+
- **Transcript search** — redacted project transcripts indexed in SQLite FTS5; `open-claudia transcript-search` gives the agent ~50ms ranked recall over months of history
|
|
21
|
+
- **Smart compaction** — long conversations are summarized proactively before they get slow; full briefs are archived to disk so nothing is truly lost (`/compact`, `/compactwindow`)
|
|
22
|
+
- **No silent learning** — every pack, entity, or skill the bot creates or changes is announced in chat with one line
|
|
23
|
+
|
|
24
|
+
### Background work
|
|
25
|
+
- **Wakeups & crons** — the agent can schedule one-shot wake-ups ("check the deploy in 20 minutes") and recurring jobs that survive restarts
|
|
26
|
+
- **Persistent tasks** — a per-channel todo tree with plans and subtasks that survives compaction and restarts
|
|
27
|
+
- **Sub-agents** — throwaway research agents for side questions that shouldn't pollute the main conversation
|
|
28
|
+
- **Agent mode** — non-blocking side conversations while heavy tasks run in the background
|
|
29
|
+
|
|
30
|
+
### Working from your phone
|
|
31
|
+
- **Voice notes** — speak instructions, transcribed locally via Whisper; voice questions get voice replies (macOS)
|
|
16
32
|
- **Screenshots & images** — send UI mockups, errors, or code screenshots
|
|
17
|
-
- **File sharing** — send PDFs, code files, documents
|
|
33
|
+
- **File sharing** — send PDFs, code files, documents; the agent can send files, photos, and voice clips back
|
|
18
34
|
- **Reply context** — reply to any message (including files) for follow-up
|
|
19
|
-
- **Streaming output** — see the agent working in real
|
|
20
|
-
|
|
21
|
-
|
|
35
|
+
- **Streaming output** — see the agent working in real time
|
|
36
|
+
|
|
37
|
+
### Team
|
|
38
|
+
- **People roster** — the bot knows your team members, their channels, and notes about them
|
|
39
|
+
- **Intros** — unknown chats go through an owner-approved introduction flow
|
|
40
|
+
- **Cross-channel relay** — the agent can message teammates on their own channels (with provenance and an audit trail)
|
|
41
|
+
- **Identity links** — link chats across channels to one canonical user so history follows you
|
|
42
|
+
|
|
43
|
+
### Operations
|
|
22
44
|
- **Encrypted vault** — store API keys and credentials securely
|
|
23
45
|
- **Customizable soul** — define your assistant's personality and knowledge
|
|
24
|
-
- **Model switching** —
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
- **
|
|
29
|
-
- **Cross-platform** —
|
|
46
|
+
- **Model switching, plan mode, effort levels, budgets** — full control from chat
|
|
47
|
+
- **Token economy** — byte-stable system prompt for maximum Anthropic prompt-cache hits; dynamic state rides each message instead
|
|
48
|
+
- **Web UI** — optional browser UI for setup and config (`open-claudia start --web`)
|
|
49
|
+
- **Auto-updates** — checks npm every 5 minutes, upgrade with `/upgrade`
|
|
50
|
+
- **Requirements doctor** — `/doctor` checks CLI installs, auth, voice tools, and writable paths
|
|
51
|
+
- **Cross-platform** — macOS, Linux, and Windows
|
|
30
52
|
|
|
31
53
|
## Prerequisites
|
|
32
54
|
|
|
33
|
-
- [Node.js](https://nodejs.org/) 18+
|
|
34
|
-
- A Telegram bot token (from [@BotFather](https://t.me/BotFather))
|
|
55
|
+
- [Node.js](https://nodejs.org/) 18+ (22.5+ recommended — its built-in SQLite powers transcript/pack/entity search; on older Node those features quietly disable themselves)
|
|
56
|
+
- A Telegram bot token (from [@BotFather](https://t.me/BotFather)) and/or a Kazee Chat bot
|
|
35
57
|
- At least one authenticated CLI backend on the host machine (see below)
|
|
36
58
|
- (Optional) [whisper.cpp](https://github.com/ggerganov/whisper.cpp) + ffmpeg for voice notes
|
|
37
59
|
|
|
@@ -63,14 +85,14 @@ agent status # Verify: should show your email and plan
|
|
|
63
85
|
```bash
|
|
64
86
|
npm install -g @openai/codex
|
|
65
87
|
codex login # Opens browser to authenticate
|
|
66
|
-
# Or from
|
|
88
|
+
# Or from chat after Open Claudia is running: /codex_login
|
|
67
89
|
# If browser/device login cannot complete remotely: /codex_setup_token
|
|
68
90
|
codex --version # Verify it works
|
|
69
91
|
```
|
|
70
92
|
|
|
71
93
|
Docker images include the Codex CLI. Direct npm installs still need optional backend CLIs installed on the host.
|
|
72
94
|
|
|
73
|
-
> **Important**: Claude Code can use macOS Keychain when you log in interactively, but a launchd/background bot may not be able to read that Keychain session. Open Claudia
|
|
95
|
+
> **Important**: Claude Code can use macOS Keychain when you log in interactively, but a launchd/background bot may not be able to read that Keychain session. Open Claudia supports `CLAUDE_CODE_OAUTH_TOKEN` for non-interactive Claude runs. Prefer `/setup_token` then `/use_oauth_token` if chat shows Claude auth/keychain errors.
|
|
74
96
|
|
|
75
97
|
### 2. Install Open Claudia
|
|
76
98
|
|
|
@@ -98,39 +120,47 @@ If setup is interrupted, running it again resumes from the last completed step.
|
|
|
98
120
|
|
|
99
121
|
All configuration is stored in `~/.open-claudia/` — survives npm upgrades.
|
|
100
122
|
|
|
123
|
+
### 4. (Optional) Add Kazee Chat
|
|
124
|
+
|
|
125
|
+
Set `CHANNELS=telegram,kazee` plus `KAZEE_URL`, `KAZEE_BOT_TOKEN`, `KAZEE_BOT_USER_ID`, and `KAZEE_OWNER_USER_ID` in `~/.open-claudia/.env`, or run `/channel add kazee` from an existing chat and follow the wizard.
|
|
126
|
+
|
|
101
127
|
## Run
|
|
102
128
|
|
|
103
129
|
```bash
|
|
104
|
-
open-claudia start
|
|
105
|
-
open-claudia
|
|
106
|
-
open-claudia
|
|
107
|
-
open-claudia
|
|
108
|
-
open-claudia
|
|
130
|
+
open-claudia start # Start the bot (--web adds the web UI, --quick skips slow health checks)
|
|
131
|
+
open-claudia web # Start with web UI for setup/config
|
|
132
|
+
open-claudia stop # Stop the bot
|
|
133
|
+
open-claudia status # Check if running (shows PID)
|
|
134
|
+
open-claudia health # Run environment health checks (-v for detail)
|
|
135
|
+
open-claudia logs # View recent logs
|
|
136
|
+
open-claudia auth # Manage chat authorizations
|
|
109
137
|
```
|
|
110
138
|
|
|
111
139
|
If installed as a background service, the bot starts automatically on login and restarts on crash.
|
|
112
140
|
|
|
113
|
-
##
|
|
141
|
+
## Chat Commands
|
|
114
142
|
|
|
115
|
-
### Backend
|
|
143
|
+
### Backend switching
|
|
116
144
|
|
|
117
145
|
| Command | Description |
|
|
118
146
|
|---------|-------------|
|
|
119
|
-
| `/cursor` | Switch to Cursor Agent backend |
|
|
120
147
|
| `/claude` | Switch to Claude Code backend |
|
|
148
|
+
| `/cursor` | Switch to Cursor Agent backend |
|
|
121
149
|
| `/codex` | Switch to OpenAI Codex backend |
|
|
122
150
|
| `/backend` | Show current backend with picker |
|
|
123
151
|
|
|
124
152
|
Each backend keeps its own persistent session. Switching doesn't lose your place — you can go back and forth freely.
|
|
125
153
|
|
|
126
|
-
### Session
|
|
154
|
+
### Session management
|
|
127
155
|
|
|
128
156
|
| Command | Description |
|
|
129
157
|
|---------|-------------|
|
|
130
|
-
| `/session` | Pick a project to work on |
|
|
158
|
+
| `/session [<project>]` | Pick a project to work on |
|
|
131
159
|
| `/sessions` | List past conversations for current project |
|
|
132
160
|
| `/projects` | Browse all workspace projects |
|
|
133
161
|
| `/continue` | Resume last conversation explicitly |
|
|
162
|
+
| `/compact` | Summarize conversation context now |
|
|
163
|
+
| `/compactwindow [<tokens> \| off \| default]` | Set the auto-compact token threshold |
|
|
134
164
|
| `/end` | End current session |
|
|
135
165
|
|
|
136
166
|
When you select a project, the last conversation is automatically resumed. Tap "New conversation" to start fresh.
|
|
@@ -139,74 +169,145 @@ When you select a project, the last conversation is automatically resumed. Tap "
|
|
|
139
169
|
|
|
140
170
|
| Command | Description |
|
|
141
171
|
|---------|-------------|
|
|
142
|
-
| `/model` | Switch model (opus / sonnet / haiku for Claude; any model flag for Cursor) |
|
|
143
|
-
| `/effort` | Set effort level
|
|
144
|
-
| `/budget` | Set max spend for next task
|
|
172
|
+
| `/model [<model>]` | Switch model (opus / sonnet / haiku for Claude; any model flag for Cursor/Codex) |
|
|
173
|
+
| `/effort [low\|medium\|high\|max]` | Set effort level |
|
|
174
|
+
| `/budget [$N]` | Set max spend for next task — Claude only |
|
|
145
175
|
| `/plan` | Toggle plan mode — `--permission-mode plan` (Claude) / `--mode plan` (Cursor) |
|
|
146
176
|
| `/ask` | Toggle ask mode — read-only Q&A, no edits (Cursor Agent only) |
|
|
147
|
-
| `/
|
|
148
|
-
| `/worktree` | Toggle isolated git branch — Claude only |
|
|
177
|
+
| `/worktree` | Toggle isolated git branch |
|
|
149
178
|
| `/mode` | Switch between direct and agent bot modes |
|
|
150
179
|
| `/status` | Show current session, backend, and settings |
|
|
180
|
+
| `/usage` | Token usage and cost for this session |
|
|
151
181
|
| `/doctor` / `/requirements` | Check Node, CLI binaries/versions/auth, voice stack, and writable paths |
|
|
152
182
|
|
|
153
|
-
###
|
|
183
|
+
### Memory & skills
|
|
154
184
|
|
|
155
185
|
| Command | Description |
|
|
156
186
|
|---------|-------------|
|
|
157
|
-
| `/
|
|
158
|
-
| `/
|
|
187
|
+
| `/learn [<hint>]` | Capture the last piece of work into the matching context pack |
|
|
188
|
+
| `/skills [show\|remove <name>]` | List, show, or remove legacy learned skills |
|
|
159
189
|
| `/soul` | View/edit assistant identity and personality |
|
|
160
190
|
|
|
161
|
-
###
|
|
191
|
+
### Automation
|
|
162
192
|
|
|
163
193
|
| Command | Description |
|
|
164
194
|
|---------|-------------|
|
|
165
|
-
| `/
|
|
166
|
-
| `/
|
|
167
|
-
| `/setup_token` | Starts `claude setup-token`; if Claude prints an OAuth token, Open Claudia stores it without echoing it |
|
|
168
|
-
| `/use_oauth_token <token>` | Stores `CLAUDE_CODE_OAUTH_TOKEN` in config for launchd/non-interactive Claude runs; the Telegram message is deleted when possible |
|
|
169
|
-
| `/use_oauth_token` | Secure pending mode: send the token as the next message and it will be deleted/stored without echo |
|
|
170
|
-
| `/clear_oauth_token` | Removes the stored OAuth token from config/process, and from vault if it is unlocked |
|
|
171
|
-
|
|
172
|
-
Tokens are redacted from Telegram output and logs. If the encrypted vault is unlocked, `/use_oauth_token` also mirrors the token into the vault, but `.env` storage is what lets launchd pass `CLAUDE_CODE_OAUTH_TOKEN` to Claude without relying on macOS Keychain.
|
|
195
|
+
| `/cron [add\|remove ...]` | Manage scheduled tasks |
|
|
196
|
+
| `/vault [create\|get\|set\|remove\|lock]` | Manage encrypted credentials (password required) |
|
|
173
197
|
|
|
174
|
-
###
|
|
198
|
+
### Team & access
|
|
175
199
|
|
|
176
200
|
| Command | Description |
|
|
177
201
|
|---------|-------------|
|
|
178
|
-
| `/
|
|
179
|
-
| `/
|
|
180
|
-
| `/
|
|
181
|
-
| `/
|
|
182
|
-
| `/
|
|
202
|
+
| `/auth [list \| revoke <chatId>]` | Request access; owner lists/revokes |
|
|
203
|
+
| `/people [show\|note\|link\|unlink\|add\|remove ...]` | Manage the team roster |
|
|
204
|
+
| `/intros [approve\|reject <id>]` | Approve/reject introductions from unknown chats |
|
|
205
|
+
| `/link [<chat-id>] <email-or-id>` | Link a chat to a canonical user id |
|
|
206
|
+
| `/links` | List identity links (owner) |
|
|
207
|
+
| `/whoami` | Show your canonical user id |
|
|
208
|
+
| `/channel [add kazee \| remove <id>]` | Add or remove channels |
|
|
183
209
|
|
|
184
|
-
|
|
210
|
+
### Claude Code auth
|
|
185
211
|
|
|
186
|
-
|
|
212
|
+
| Command | Description |
|
|
213
|
+
|---------|-------------|
|
|
214
|
+
| `/auth_status` | Reports redacted Claude auth status and whether an OAuth token is configured |
|
|
215
|
+
| `/login` | Starts a Claude login flow, sends the URL/code, accepts paste-back codes (`/auth_code <code>`, `/cancel_auth`) |
|
|
216
|
+
| `/setup_token` | Runs `claude setup-token`; the resulting OAuth token is stored without echoing it |
|
|
217
|
+
| `/use_oauth_token [<token>]` | Stores `CLAUDE_CODE_OAUTH_TOKEN` for non-interactive runs; the message is deleted when possible. Without an argument, send the token as the next message |
|
|
218
|
+
| `/clear_oauth_token` | Removes the stored OAuth token |
|
|
187
219
|
|
|
188
|
-
|
|
220
|
+
Tokens are redacted from chat output and logs.
|
|
189
221
|
|
|
190
|
-
|
|
191
|
-
- Claude CLI version/auth
|
|
192
|
-
- Cursor Agent version/status when installed/configured
|
|
193
|
-
- Codex CLI version/auth when installed/configured
|
|
194
|
-
- ffmpeg/Whisper/model paths when voice is configured
|
|
195
|
-
- Workspace and config directory writability
|
|
222
|
+
### Codex auth
|
|
196
223
|
|
|
197
|
-
|
|
224
|
+
| Command | Description |
|
|
225
|
+
|---------|-------------|
|
|
226
|
+
| `/codex_auth_status` | Reports redacted Codex auth status/version |
|
|
227
|
+
| `/codex_login` | Starts `codex login --device-auth` and relays the URL/device code |
|
|
228
|
+
| `/codex_setup_token [<key>]` | Secure paste mode for an OpenAI API key (message deleted, never echoed) |
|
|
229
|
+
| `/cancel_codex_auth` | Cancels a pending Codex auth flow |
|
|
198
230
|
|
|
199
231
|
### System
|
|
200
232
|
|
|
201
233
|
| Command | Description |
|
|
202
234
|
|---------|-------------|
|
|
203
235
|
| `/version` | Show current running version |
|
|
204
|
-
| `/
|
|
205
|
-
| `/upgrade` | Upgrade to latest version and restart |
|
|
236
|
+
| `/upgrade` | Upgrade to latest version and restart (includes a post-upgrade doctor summary) |
|
|
206
237
|
| `/restart` | Restart the bot |
|
|
207
238
|
| `/stop` | Cancel a running task |
|
|
208
239
|
| `/help` | Show all commands |
|
|
209
240
|
|
|
241
|
+
## Memory & Long-Term Context
|
|
242
|
+
|
|
243
|
+
Open Claudia layers three memory systems on top of the backend's native sessions:
|
|
244
|
+
|
|
245
|
+
**Context packs** (`~/.open-claudia/packs/<dir>/PACK.md`) are living per-topic documents with four sections: *Stance* (how to think about the topic — your preferences and hard rules), *Procedure* (verified how-to steps), *State* (where work stands now), and *Journal* (a dated one-line log of past sessions). Incoming messages are matched against packs (FTS5, field-weighted so a stray word can't drag a pack in) and hits are injected into the agent's context — mention a project anywhere and the assistant picks up its train of thought, decisions, and history without you re-explaining. After every substantial turn, a background reviewer on a cheap model updates the relevant pack (or creates one for a genuinely new topic). Every change is announced in chat.
|
|
246
|
+
|
|
247
|
+
**Entity memory** (`~/.open-claudia/entities/<slug>.md`) works the same way for the people, places, projects, orgs, and systems you mention — who they are, current truth, and a dated observation log. Mentioning a name injects its note.
|
|
248
|
+
|
|
249
|
+
**Dream consolidation** — while the per-turn reviewer takes quick notes, *dream* is the slow overnight pass (default 4am, on a stronger model): it merges packs that drifted into the same topic, builds parent/sub pack trees with umbrella summaries, tightens descriptions and tags so the router matches with less noise, dedupes entities, and cross-links notes. Anything merged away is backed up under `~/.open-claudia/backup/dream-<stamp>/` first, and every dream that changes something reports in chat. Configure with `DREAM_CRON` / `DREAM_MODEL`, disable with `DREAM=off`.
|
|
250
|
+
|
|
251
|
+
**Personality** — your `soul.md` holds identity and hard rules; `~/.open-claudia/persona.md` holds the voice on top — tone, quirks, emoji habits. It feeds into the system prompt and the dream pass may evolve it gently (bounded, backed up, announced). Edit it directly any time.
|
|
252
|
+
|
|
253
|
+
**Transcript index** — every project conversation is logged (redacted) to JSONL outside the repo and indexed in SQLite FTS5, giving the agent ranked search over your full history.
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
open-claudia pack list|show <dir>|match "<text>"|migrate|remove <dir>|reindex
|
|
257
|
+
open-claudia entity list|show <slug>|match "<text>"|note <name> "<text>"|remove <slug>|reindex
|
|
258
|
+
open-claudia dream [--dry-run] # run the consolidation pass now
|
|
259
|
+
open-claudia transcript-search "<query>" # alias: ts; --all for every project
|
|
260
|
+
open-claudia transcript-window "<pattern>" # alias: tw; hits with surrounding turns
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`pack migrate` folds legacy `~/.claude/skills` into packs (originals backed up, never deleted) — run it once after upgrading from a pre-2.5 version.
|
|
264
|
+
|
|
265
|
+
When a conversation grows past the auto-compact threshold, the bot summarizes it proactively: the full brief is archived to `~/.open-claudia/briefs/` and a condensed version seeds the fresh session, so long-running work never hits a context wall.
|
|
266
|
+
|
|
267
|
+
## Background Work
|
|
268
|
+
|
|
269
|
+
The agent can schedule its own future work — these survive bot restarts:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
open-claudia schedule-wakeup <30s|5m|2h|1d|ISO-date> "<prompt>" # one-shot; resumes the session
|
|
273
|
+
open-claudia cron-add "<5-field cron>" "<prompt>" # recurring
|
|
274
|
+
open-claudia cron-list / cron-remove <id>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
A persistent per-channel task tree tracks multi-turn work (plans with subtasks; completed items are removed automatically):
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
open-claudia task plan "<title>" "<step 1>" "<step 2>" [--description "..."]
|
|
281
|
+
open-claudia task add|list|start <id>|done <id>|remove <id>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
And for side research that shouldn't pollute the main conversation:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
open-claudia agent "<prompt>" [--role "<role>"] # throwaway sub-agent, output on stdout
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Team & Cross-Channel
|
|
291
|
+
|
|
292
|
+
The setup owner is automatically authorized. To add more users:
|
|
293
|
+
|
|
294
|
+
- **From chat**: unknown users who message the bot go through an intro flow — the owner gets an approve/reject prompt (`/intros`). Or they send `/auth` for a direct request.
|
|
295
|
+
- **From the terminal**: `open-claudia auth` shows authorized chats and pending requests.
|
|
296
|
+
|
|
297
|
+
Each chat defaults to `telegram:<chatId>` as its user id. Link chats to a canonical id to share sessions and history across channels:
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
/link sumeet@example.com
|
|
301
|
+
/whoami
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The bot keeps a people roster (`/people`, or `open-claudia people list|show|note|link`) and can relay messages between team members with provenance and an audit trail:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
open-claudia send-to --person "<name>" "<message>"
|
|
308
|
+
open-claudia recent --person "<name>" [--limit 20] # read recent activity from another chat
|
|
309
|
+
```
|
|
310
|
+
|
|
210
311
|
## Backend Comparison
|
|
211
312
|
|
|
212
313
|
| | Claude Code | Cursor Agent | OpenAI Codex |
|
|
@@ -228,11 +329,13 @@ All three backends output structured JSON which Open Claudia parses for real-tim
|
|
|
228
329
|
|
|
229
330
|
Send any file to the bot — PDFs, code files, documents, images. Files are saved to `~/.open-claudia/files/` with their original names. The agent reads the file and responds based on content.
|
|
230
331
|
|
|
231
|
-
Add a caption to
|
|
332
|
+
Add a caption to give the agent specific instructions:
|
|
232
333
|
- Send a PDF with caption "summarize the key findings"
|
|
233
334
|
- Send a code file with caption "find bugs in this"
|
|
234
335
|
- Send a screenshot with caption "implement this design"
|
|
235
336
|
|
|
337
|
+
The agent can also send files back — documents, photos with inline previews, and voice clips — directly from within a task.
|
|
338
|
+
|
|
236
339
|
## Voice Notes
|
|
237
340
|
|
|
238
341
|
Requires [whisper.cpp](https://github.com/ggerganov/whisper.cpp) and ffmpeg:
|
|
@@ -246,42 +349,22 @@ sudo apt install ffmpeg
|
|
|
246
349
|
# Build whisper.cpp from source: https://github.com/ggerganov/whisper.cpp
|
|
247
350
|
```
|
|
248
351
|
|
|
249
|
-
Voice notes are transcribed locally — nothing sent to external services.
|
|
250
|
-
|
|
251
|
-
## Multi-User Authorization
|
|
252
|
-
|
|
253
|
-
The setup owner is automatically authorized. To add more users:
|
|
254
|
-
|
|
255
|
-
**From Telegram** (unauthorized users):
|
|
256
|
-
- Send `/auth` to the bot — the owner gets an Approve/Deny button prompt in Telegram
|
|
257
|
-
|
|
258
|
-
**From the terminal** (owner):
|
|
259
|
-
```bash
|
|
260
|
-
open-claudia auth
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
This shows authorized chats, pending requests, and lets you approve/deny or add new users with code verification. Telegram button approvals use the same `auth.json` authorization list.
|
|
264
|
-
|
|
265
|
-
Authorized chats are separate from identity links. By default each Telegram chat uses `telegram:<chatId>` as its user id. To share one session/history across channels later, link the chat to a canonical id:
|
|
266
|
-
|
|
267
|
-
```text
|
|
268
|
-
/link sumeet@inet.africa
|
|
269
|
-
/whoami
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
The owner can link another Telegram chat with `/link <chatId> <email-or-user-id>` and list explicit links with `/links`.
|
|
352
|
+
Voice notes are transcribed locally — nothing sent to external services. On macOS, replies to voice messages come back as voice notes too.
|
|
273
353
|
|
|
274
354
|
## How It Works
|
|
275
355
|
|
|
276
356
|
```
|
|
277
|
-
Phone (Telegram) --> Bot (Node.js) --> Claude Code CLI
|
|
278
|
-
|
|
279
|
-
|
|
357
|
+
Phone (Telegram / Kazee) --> Bot (Node.js) --> Claude Code CLI --> Your codebase
|
|
358
|
+
--> Cursor Agent CLI -->
|
|
359
|
+
--> OpenAI Codex CLI -->
|
|
360
|
+
<-- <-- <--
|
|
280
361
|
```
|
|
281
362
|
|
|
282
|
-
The bot spawns the active backend CLI in headless mode (`--print` / `-p`) for each message, streaming `stream-json`/JSONL output back to
|
|
363
|
+
The bot spawns the active backend CLI in headless mode (`--print` / `-p`) for each message, streaming `stream-json`/JSONL output back to the chat. It maintains native backend context via `--resume` and passes a system prompt that gives the agent awareness of the chat environment, configuration files, memory systems, and the ability to send files/images directly.
|
|
283
364
|
|
|
284
|
-
|
|
365
|
+
The appended system prompt is byte-stable within a session to maximize Anthropic prompt-cache hits; per-turn state (vault status, pending tasks, matched packs/entities) rides the user message instead, where it is always uncached anyway.
|
|
366
|
+
|
|
367
|
+
Open Claudia does not summarize one backend into another during switches; instead it records a redacted project transcript outside the repo and injects a small pointer telling fresh/switched sessions to search it only if needed.
|
|
285
368
|
|
|
286
369
|
## Configuration Files
|
|
287
370
|
|
|
@@ -289,36 +372,57 @@ All stored in `~/.open-claudia/`:
|
|
|
289
372
|
|
|
290
373
|
| File | Purpose |
|
|
291
374
|
|------|---------|
|
|
292
|
-
| `.env` |
|
|
375
|
+
| `.env` | Tokens, workspace path, binary paths, channel config |
|
|
293
376
|
| `auth.json` | Authorized users and pending requests |
|
|
294
|
-
| `identities.json` | Channel-to-canonical-user mappings
|
|
377
|
+
| `identities.json` | Channel-to-canonical-user mappings |
|
|
378
|
+
| `people.json` / `intros.json` | Team roster and pending introductions |
|
|
295
379
|
| `vault.enc` | Encrypted credential store |
|
|
296
|
-
| `soul.md` | Assistant identity and
|
|
297
|
-
| `
|
|
380
|
+
| `soul.md` | Assistant identity and hard rules (editable via `/soul`) |
|
|
381
|
+
| `persona.md` | Assistant voice and quirks (feeds the system prompt; evolved gently by dream) |
|
|
382
|
+
| `crons.json` / `jobs.json` | Recurring crons and one-shot wakeups |
|
|
383
|
+
| `tasks/` | Persistent per-channel task trees |
|
|
384
|
+
| `packs/` | Context packs (living topic documents) + FTS index |
|
|
385
|
+
| `entities/` | Entity notes (people/places/projects) + FTS index |
|
|
298
386
|
| `sessions.json` | Per-project conversation history |
|
|
299
387
|
| `state.json` | Current session state including active backend (survives restarts) |
|
|
388
|
+
| `transcripts/` | Redacted project-scoped JSONL transcripts + FTS index |
|
|
389
|
+
| `briefs/` | Archived full compaction briefs |
|
|
390
|
+
| `audit.log` | Relay/intro/auth audit trail |
|
|
300
391
|
| `bot.log` | Bot logs |
|
|
301
|
-
| `
|
|
302
|
-
| `files/` | Files received from Telegram |
|
|
392
|
+
| `files/` | Files received from chat |
|
|
303
393
|
| `media/` | Temporary media (voice notes, photos) |
|
|
394
|
+
| `backup/` | Backups (skills folded by `pack migrate`; packs/entities merged away by dream) |
|
|
304
395
|
|
|
305
396
|
### Environment Variables (.env)
|
|
306
397
|
|
|
307
398
|
| Variable | Required | Description |
|
|
308
399
|
|----------|----------|-------------|
|
|
309
|
-
| `TELEGRAM_BOT_TOKEN` | Yes | Bot token from BotFather |
|
|
310
|
-
| `TELEGRAM_CHAT_ID` | Yes | Comma-separated authorized chat IDs |
|
|
400
|
+
| `TELEGRAM_BOT_TOKEN` | Yes* | Bot token from BotFather |
|
|
401
|
+
| `TELEGRAM_CHAT_ID` | Yes* | Comma-separated authorized chat IDs |
|
|
402
|
+
| `CHANNELS` | No | Channels to start: `telegram`, `kazee`, or `telegram,kazee` (default `telegram`) |
|
|
403
|
+
| `KAZEE_URL` / `KAZEE_BOT_TOKEN` / `KAZEE_BOT_USER_ID` / `KAZEE_OWNER_USER_ID` | Kazee only | Kazee Chat connection |
|
|
311
404
|
| `WORKSPACE` | Yes | Path to your projects directory |
|
|
312
405
|
| `CLAUDE_PATH` | Yes | Path to Claude Code CLI binary |
|
|
313
|
-
| `CURSOR_PATH` | No |
|
|
314
|
-
| `
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
317
|
-
| `
|
|
318
|
-
| `
|
|
319
|
-
| `
|
|
406
|
+
| `CURSOR_PATH` / `CODEX_PATH` | No | Paths to optional backend CLIs (auto-detected if in PATH) |
|
|
407
|
+
| `CLAUDE_CODE_OAUTH_TOKEN` | No | OAuth token for non-interactive Claude runs (set via `/use_oauth_token`) |
|
|
408
|
+
| `CLAUDE_MODEL` | No | Default Claude model |
|
|
409
|
+
| `AUTO_COMPACT_TOKENS` | No | Auto-compact threshold in tokens (also settable via `/compactwindow`) |
|
|
410
|
+
| `PROJECT_TRANSCRIPTS` | No | Enable redacted project transcripts (default `true`) |
|
|
411
|
+
| `TRANSCRIPT_MAX_ENTRY_CHARS` | No | Max chars per transcript entry (default `12000`) |
|
|
412
|
+
| `TRANSCRIPTS_DIR` / `PACKS_DIR` / `ENTITIES_DIR` | No | Override storage directories |
|
|
413
|
+
| `PACK_REVIEW` | No | `off` disables the post-turn pack/entity reviewer (default on) |
|
|
414
|
+
| `PACK_REVIEW_MODEL` | No | Model for the reviewer (default `haiku`) |
|
|
415
|
+
| `PACK_MATCH_THRESHOLD` / `ENTITY_MATCH_THRESHOLD` | No | Router match score thresholds (default `2`) |
|
|
416
|
+
| `DREAM` | No | `off` disables the nightly memory consolidation pass (default on) |
|
|
417
|
+
| `DREAM_CRON` | No | Schedule for the dream pass (default `0 4 * * *`) |
|
|
418
|
+
| `DREAM_MODEL` | No | Model for the dream pass (default `sonnet`) |
|
|
419
|
+
| `PERSONA_FILE` | No | Override the persona file location |
|
|
420
|
+
| `WEB_UI` / `WEB_PORT` / `WEB_PASSWORD` | No | Web UI toggle, port, and password |
|
|
421
|
+
| `WHISPER_CLI` / `WHISPER_MODEL` | No | whisper.cpp binary and model for voice notes |
|
|
320
422
|
| `FFMPEG` | No | Path to ffmpeg binary |
|
|
321
423
|
|
|
424
|
+
\* Required for the Telegram channel.
|
|
425
|
+
|
|
322
426
|
## Background Service
|
|
323
427
|
|
|
324
428
|
### macOS (launchd)
|
|
@@ -346,14 +450,7 @@ sudo systemctl status claude-telegram-bot
|
|
|
346
450
|
|
|
347
451
|
## Auto-Updates
|
|
348
452
|
|
|
349
|
-
The bot checks npm for new versions every 5 minutes. When an update is available, you get a
|
|
350
|
-
|
|
351
|
-
> "Hey! A new version is available (v1.10.0). You're on v1.9.2."
|
|
352
|
-
|
|
353
|
-
Send `/upgrade` to update. The bot will:
|
|
354
|
-
1. Download and install the new version
|
|
355
|
-
2. Go offline briefly
|
|
356
|
-
3. Restart and notify you it's back
|
|
453
|
+
The bot checks npm for new versions every 5 minutes. When an update is available, you get a chat notification. Send `/upgrade` to update — the bot installs the new version, restarts, runs a doctor check, and notifies you it's back.
|
|
357
454
|
|
|
358
455
|
For direct npm installs, `/upgrade` updates Open Claudia itself and does not install optional backend CLIs such as Codex or Cursor Agent. Container deployments should roll out a new Docker image when bundled CLI tools change.
|
|
359
456
|
|
|
@@ -367,7 +464,7 @@ Schedule recurring tasks:
|
|
|
367
464
|
/cron add "*/30 * * * *" myproject "Health check: verify the API is responding"
|
|
368
465
|
```
|
|
369
466
|
|
|
370
|
-
Presets available via `/cron` menu.
|
|
467
|
+
Presets available via `/cron` menu. The agent can also schedule its own jobs with `open-claudia cron-add` / `schedule-wakeup` (see Background Work).
|
|
371
468
|
|
|
372
469
|
## Vault
|
|
373
470
|
|
package/bin/cli.js
CHANGED
|
@@ -289,6 +289,11 @@ switch (command) {
|
|
|
289
289
|
break;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
case "dream": {
|
|
293
|
+
require("./dream").run(args.slice(1));
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
|
|
292
297
|
case "people": {
|
|
293
298
|
require("./people").run(args.slice(1));
|
|
294
299
|
break;
|
|
@@ -332,6 +337,7 @@ Memory tools:
|
|
|
332
337
|
(alias: tw; --help for options)
|
|
333
338
|
open-claudia pack list|show|match|migrate Context packs: living topic docs (skills + memory)
|
|
334
339
|
open-claudia entity list|show|match|note Entity notes: people/places/projects memory
|
|
340
|
+
open-claudia dream [--dry-run] Run the memory consolidation pass now
|
|
335
341
|
|
|
336
342
|
Background work (only inside an active bot-spawned task):
|
|
337
343
|
open-claudia schedule-wakeup <when> "<prompt>" One-shot future wake-up; resumes session
|
package/bin/dream.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// open-claudia dream [--dry-run]
|
|
2
|
+
// Manually trigger the memory consolidation pass (normally runs on the
|
|
3
|
+
// quiet-hours cron inside the bot). Prints the report; from inside a
|
|
4
|
+
// bot-spawned task the parent agent can relay it to chat.
|
|
5
|
+
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
8
|
+
const HELP = `
|
|
9
|
+
Run the dream memory-consolidation pass now.
|
|
10
|
+
|
|
11
|
+
open-claudia dream [--dry-run]
|
|
12
|
+
|
|
13
|
+
Reviews all context packs and entities with a stronger model (DREAM_MODEL,
|
|
14
|
+
default sonnet): merges duplicate packs, builds umbrella/parent trees,
|
|
15
|
+
tightens descriptions and tags, dedupes entities, cross-links notes, and
|
|
16
|
+
may gently evolve the persona. Merged-away packs/entities are backed up
|
|
17
|
+
under ~/.open-claudia/backup/dream-<stamp>/ first.
|
|
18
|
+
|
|
19
|
+
--dry-run Ask the model for the decision and print it without applying.
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
async function run(args) {
|
|
23
|
+
if (args[0] === "--help" || args[0] === "help") { console.log(HELP); process.exit(0); }
|
|
24
|
+
const dream = require(path.join(__dirname, "..", "core", "dream"));
|
|
25
|
+
|
|
26
|
+
if (args.includes("--dry-run")) {
|
|
27
|
+
const { spawnSubagent } = require(path.join(__dirname, "..", "core", "subagent"));
|
|
28
|
+
console.error("Dreaming (dry run)… this can take a minute or two.");
|
|
29
|
+
try {
|
|
30
|
+
const { text } = await spawnSubagent(dream.buildDreamPrompt(), {
|
|
31
|
+
model: dream.DREAM_MODEL,
|
|
32
|
+
timeoutMs: 8 * 60 * 1000,
|
|
33
|
+
systemPrompt: "You are a background memory consolidation process. Reply with ONLY the requested JSON object. No prose, no markdown, no tool use.",
|
|
34
|
+
});
|
|
35
|
+
const decision = dream.parseDream(text);
|
|
36
|
+
if (!decision) { console.error("Unparseable model output:"); console.log(text); process.exit(1); }
|
|
37
|
+
console.log(JSON.stringify(decision, null, 2));
|
|
38
|
+
process.exit(0);
|
|
39
|
+
} catch (e) { console.error(`dream failed: ${e.message}`); process.exit(1); }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.error("Dreaming… this can take a minute or two.");
|
|
43
|
+
try {
|
|
44
|
+
const res = await dream.runDream({ trigger: "cli" });
|
|
45
|
+
if (res.skipped) { console.log(`Skipped: ${res.skipped}`); process.exit(0); }
|
|
46
|
+
console.log(res.message || `💤 Nothing needed tidying — memory is in good shape. (${res.report || "no report"})`);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
} catch (e) { console.error(`dream failed: ${e.message}`); process.exit(1); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { run, HELP };
|
package/bot.js
CHANGED
|
@@ -161,6 +161,8 @@ setInterval(checkForUpdates, 5 * 60 * 1000);
|
|
|
161
161
|
} catch (e) {}
|
|
162
162
|
|
|
163
163
|
initScheduler(adapters);
|
|
164
|
+
try { require("./core/dream").initDream(adapters); }
|
|
165
|
+
catch (e) { console.error("dream init failed:", e.message); }
|
|
164
166
|
|
|
165
167
|
try {
|
|
166
168
|
const lb = await loopback.start(registry);
|
package/core/dream.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// Dream: scheduled memory consolidation (Phase 3). While the per-turn
|
|
2
|
+
// reviewer takes quick notes, dream is the slow pass that runs in the
|
|
3
|
+
// quiet hours: it merges packs that drifted into the same topic, builds
|
|
4
|
+
// parent/sub pack trees with umbrella summaries, tightens descriptions
|
|
5
|
+
// and tags (the FTS router matches on those, so tighter = less noise),
|
|
6
|
+
// deduplicates entities, cross-links entity notes to packs, and gently
|
|
7
|
+
// evolves the persona based on how recent work actually went. The model
|
|
8
|
+
// only RETURNS a JSON decision — all writes are applied here, every
|
|
9
|
+
// merged-away pack/entity is backed up first, and the result is
|
|
10
|
+
// reported in chat (no silent learning).
|
|
11
|
+
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const cron = require("node-cron");
|
|
15
|
+
|
|
16
|
+
const CONFIG_DIR = require("../config-dir");
|
|
17
|
+
const packs = require("./packs");
|
|
18
|
+
const entities = require("./entities");
|
|
19
|
+
const persona = require("./persona");
|
|
20
|
+
const { spawnSubagent } = require("./subagent");
|
|
21
|
+
|
|
22
|
+
const DREAM_MODEL = process.env.DREAM_MODEL || "sonnet";
|
|
23
|
+
const DREAM_CRON = process.env.DREAM_CRON || "0 4 * * *";
|
|
24
|
+
const MAX_PACK_CHARS = 2500;
|
|
25
|
+
const MAX_ENTITY_CHARS = 900;
|
|
26
|
+
const LIMITS = { merges: 3, umbrellas: 2, parents: 8, retag: 8, entity_merges: 3, entity_notes: 4 };
|
|
27
|
+
|
|
28
|
+
let _dreaming = false;
|
|
29
|
+
|
|
30
|
+
function enabled() {
|
|
31
|
+
return (process.env.DREAM || "on").toLowerCase() !== "off";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function clip(s, n) {
|
|
35
|
+
const t = String(s || "");
|
|
36
|
+
return t.length > n ? t.slice(0, n) + "\n…[truncated]" : t;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildDreamPrompt() {
|
|
40
|
+
const allPacks = packs.listPacks();
|
|
41
|
+
const allEntities = entities.listEntities();
|
|
42
|
+
|
|
43
|
+
const packDump = allPacks.map((p) => clip([
|
|
44
|
+
`=== pack: ${p.dir}${p.parent ? ` (parent: ${p.parent})` : ""}`,
|
|
45
|
+
`name: ${p.name}`,
|
|
46
|
+
`description: ${p.description}`,
|
|
47
|
+
`tags: ${p.tags.join(", ")}`,
|
|
48
|
+
`last_used: ${p.last_used || "never"}`,
|
|
49
|
+
`Stance: ${p.sections.Stance}`,
|
|
50
|
+
`Procedure: ${p.sections.Procedure}`,
|
|
51
|
+
`State: ${p.sections.State}`,
|
|
52
|
+
`Journal:\n${p.sections.Journal}`,
|
|
53
|
+
].join("\n"), MAX_PACK_CHARS)).join("\n\n") || "(none)";
|
|
54
|
+
|
|
55
|
+
const entityDump = allEntities.map((e) => clip([
|
|
56
|
+
`=== entity: ${e.slug} (${e.type}${e.aliases.length ? `, aka ${e.aliases.join(", ")}` : ""})`,
|
|
57
|
+
`description: ${e.description}`,
|
|
58
|
+
`Notes: ${e.sections.Notes}`,
|
|
59
|
+
`Log:\n${e.sections.Log}`,
|
|
60
|
+
].join("\n"), MAX_ENTITY_CHARS)).join("\n\n") || "(none)";
|
|
61
|
+
|
|
62
|
+
return `You are the dream pass of a personal AI assistant called Open Claudia — the overnight consolidation of her long-term memory. Below is her entire memory: context packs (living topic documents) and entity notes (people/places/projects/orgs/systems), plus her current persona.
|
|
63
|
+
|
|
64
|
+
Today: ${new Date().toISOString().slice(0, 10)}
|
|
65
|
+
|
|
66
|
+
CONTEXT PACKS:
|
|
67
|
+
|
|
68
|
+
${packDump}
|
|
69
|
+
|
|
70
|
+
ENTITIES:
|
|
71
|
+
|
|
72
|
+
${entityDump}
|
|
73
|
+
|
|
74
|
+
CURRENT PERSONA:
|
|
75
|
+
|
|
76
|
+
${persona.loadPersona()}
|
|
77
|
+
|
|
78
|
+
Your job — decide what consolidation, if any, is warranted:
|
|
79
|
+
|
|
80
|
+
1. merges: packs that are clearly the SAME topic under different names get merged into one. Supply the merged Stance/Procedure/State (synthesised, not concatenated; null leaves the target's section alone) and a one-sentence journal note. Be conservative: when in doubt, do not merge.
|
|
81
|
+
2. umbrellas: when 3+ packs are siblings under one theme, create an umbrella pack whose State is a 3-6 line map of the family ("for X see pack Y"), and list its children. The umbrella is a router, not a duplicate.
|
|
82
|
+
3. parents: assign an existing pack as parent of another (sub-topic relationship) without creating anything.
|
|
83
|
+
4. retag: tighten descriptions and tags. The router FTS-matches incoming messages against name/description/tags, so generic words there cause false matches. Descriptions should be one specific line; tags specific nouns.
|
|
84
|
+
5. entity_merges: the same real-world entity recorded twice gets merged (the better slug wins).
|
|
85
|
+
6. entity_notes: rewrite an entity's Notes to be current and cross-linked — mention related packs as [[pack-dir]] and related entities by name.
|
|
86
|
+
7. persona: evolve the persona GENTLY — keep its structure and length (under 2200 chars), adjust only what recent work justifies (a new habit, a sharpened quirk). Most dreams should return null here.
|
|
87
|
+
8. report: a short chat message to the owner, written AS Open Claudia in first person — warm, a little playful, a few emojis, mobile-friendly (2-6 short lines). Say what you tidied and why it helps. If you changed nothing, say the memory is in good shape, charmingly.
|
|
88
|
+
|
|
89
|
+
Hard rules:
|
|
90
|
+
- Never invent packs/entities not listed above; reference them by exact dir/slug.
|
|
91
|
+
- Never store secrets, tokens, passwords, or credentials anywhere.
|
|
92
|
+
- Limits: ≤${LIMITS.merges} merges, ≤${LIMITS.umbrellas} umbrellas, ≤${LIMITS.parents} parents, ≤${LIMITS.retag} retags, ≤${LIMITS.entity_merges} entity merges, ≤${LIMITS.entity_notes} entity note rewrites.
|
|
93
|
+
- An empty decision is a perfectly good decision.
|
|
94
|
+
|
|
95
|
+
Reply with ONLY a JSON object, no prose, no code fences:
|
|
96
|
+
{"merges": [{"into": "<dir>", "from": ["<dir>"], "stance": null, "procedure": null, "state": null, "journal": "<one sentence>"}],
|
|
97
|
+
"umbrellas": [{"dir": "<new-kebab-slug>", "name": "<title>", "description": "<one specific line>", "tags": ["..."], "state": "<3-6 line family map>", "children": ["<dir>"]}],
|
|
98
|
+
"parents": [{"pack": "<dir>", "parent": "<dir>"}],
|
|
99
|
+
"retag": [{"pack": "<dir>", "description": "<one specific line>", "tags": ["..."]}],
|
|
100
|
+
"entity_merges": [{"into": "<slug>", "from": ["<slug>"], "notes": "<merged Notes or null>"}],
|
|
101
|
+
"entity_notes": [{"entity": "<slug>", "notes": "<rewritten Notes>"}],
|
|
102
|
+
"persona": null,
|
|
103
|
+
"report": "<chat message>"}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseDream(text) {
|
|
107
|
+
const raw = String(text || "").trim().replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, "");
|
|
108
|
+
const start = raw.indexOf("{");
|
|
109
|
+
const end = raw.lastIndexOf("}");
|
|
110
|
+
if (start === -1 || end <= start) return null;
|
|
111
|
+
try {
|
|
112
|
+
const obj = JSON.parse(raw.slice(start, end + 1));
|
|
113
|
+
const arr = (k) => (Array.isArray(obj[k]) ? obj[k].slice(0, LIMITS[k] || 8) : []);
|
|
114
|
+
return {
|
|
115
|
+
merges: arr("merges"),
|
|
116
|
+
umbrellas: arr("umbrellas"),
|
|
117
|
+
parents: arr("parents"),
|
|
118
|
+
retag: arr("retag"),
|
|
119
|
+
entity_merges: arr("entity_merges"),
|
|
120
|
+
entity_notes: arr("entity_notes"),
|
|
121
|
+
persona: typeof obj.persona === "string" ? obj.persona : null,
|
|
122
|
+
report: typeof obj.report === "string" ? obj.report.trim() : "",
|
|
123
|
+
};
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function makeBackupRoot() {
|
|
130
|
+
const stamp = new Date().toISOString().slice(0, 16).replace(/[:T]/g, "-");
|
|
131
|
+
return path.join(CONFIG_DIR, "backup", `dream-${stamp}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function backupPack(dir, root) {
|
|
135
|
+
fs.cpSync(path.join(packs.PACKS_DIR, dir), path.join(root, "packs", dir), { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function backupEntity(slug, root) {
|
|
139
|
+
const dest = path.join(root, "entities");
|
|
140
|
+
fs.mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
141
|
+
fs.copyFileSync(path.join(entities.ENTITIES_DIR, slug + ".md"), path.join(dest, slug + ".md"));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function wouldCycle(packDir, parentDir) {
|
|
145
|
+
let cur = parentDir;
|
|
146
|
+
for (let i = 0; i < 10 && cur; i++) {
|
|
147
|
+
if (cur === packDir) return true;
|
|
148
|
+
cur = packs.readPack(cur)?.parent || null;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Applies a parsed dream decision. Returns one human line per applied
|
|
154
|
+
// change; invalid items are skipped, never thrown.
|
|
155
|
+
function applyDream(decision, backupRoot) {
|
|
156
|
+
const lines = [];
|
|
157
|
+
const gone = new Set(); // dirs/slugs removed this run
|
|
158
|
+
|
|
159
|
+
for (const m of decision.merges) {
|
|
160
|
+
try {
|
|
161
|
+
const into = m?.into && packs.readPack(m.into);
|
|
162
|
+
const from = [].concat(m?.from || []).filter((d) => d && d !== m.into && !gone.has(d) && packs.readPack(d));
|
|
163
|
+
if (!into || from.length === 0) continue;
|
|
164
|
+
packs.updatePack(m.into, {
|
|
165
|
+
stance: typeof m.stance === "string" ? m.stance : "",
|
|
166
|
+
procedure: typeof m.procedure === "string" ? m.procedure : "",
|
|
167
|
+
state: typeof m.state === "string" ? m.state : "",
|
|
168
|
+
journal: m.journal || `absorbed ${from.join(", ")}`,
|
|
169
|
+
});
|
|
170
|
+
for (const d of from) {
|
|
171
|
+
backupPack(d, backupRoot);
|
|
172
|
+
packs.removePack(d);
|
|
173
|
+
gone.add(d);
|
|
174
|
+
}
|
|
175
|
+
lines.push(`📦 Merged ${from.join(" + ")} into ${m.into}`);
|
|
176
|
+
} catch (e) { console.warn(`[dream] merge failed: ${e.message}`); }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const u of decision.umbrellas) {
|
|
180
|
+
try {
|
|
181
|
+
const dir = packs.slugify(u?.dir || u?.name);
|
|
182
|
+
if (!dir || gone.has(dir)) continue;
|
|
183
|
+
const children = [].concat(u?.children || []).filter((d) => d && d !== dir && !gone.has(d) && packs.readPack(d));
|
|
184
|
+
if (children.length < 2) continue;
|
|
185
|
+
if (!packs.readPack(dir)) {
|
|
186
|
+
packs.createPack({
|
|
187
|
+
dir,
|
|
188
|
+
name: u.name || dir,
|
|
189
|
+
description: u.description || "",
|
|
190
|
+
tags: Array.isArray(u.tags) ? u.tags.slice(0, 6) : [],
|
|
191
|
+
state: u.state || "",
|
|
192
|
+
journal: `umbrella created over ${children.join(", ")}`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
for (const c of children) {
|
|
196
|
+
const child = packs.readPack(c);
|
|
197
|
+
if (!child || wouldCycle(c, dir)) continue;
|
|
198
|
+
child.parent = dir;
|
|
199
|
+
child.updated = new Date().toISOString();
|
|
200
|
+
packs.writePack(child);
|
|
201
|
+
}
|
|
202
|
+
lines.push(`🌂 Umbrella pack ${dir} now covers ${children.join(", ")}`);
|
|
203
|
+
} catch (e) { console.warn(`[dream] umbrella failed: ${e.message}`); }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const p of decision.parents) {
|
|
207
|
+
try {
|
|
208
|
+
const child = p?.pack && !gone.has(p.pack) && packs.readPack(p.pack);
|
|
209
|
+
if (!child || !p.parent || gone.has(p.parent) || !packs.readPack(p.parent)) continue;
|
|
210
|
+
if (p.pack === p.parent || wouldCycle(p.pack, p.parent)) continue;
|
|
211
|
+
if (child.parent === p.parent) continue;
|
|
212
|
+
child.parent = p.parent;
|
|
213
|
+
child.updated = new Date().toISOString();
|
|
214
|
+
packs.writePack(child);
|
|
215
|
+
lines.push(`🌳 Filed ${p.pack} under ${p.parent}`);
|
|
216
|
+
} catch (e) { console.warn(`[dream] parent failed: ${e.message}`); }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const r of decision.retag) {
|
|
220
|
+
try {
|
|
221
|
+
if (!r?.pack || gone.has(r.pack) || !packs.readPack(r.pack)) continue;
|
|
222
|
+
const desc = typeof r.description === "string" ? r.description.trim() : "";
|
|
223
|
+
const tags = Array.isArray(r.tags) ? r.tags.map((t) => String(t).trim()).filter(Boolean).slice(0, 6) : [];
|
|
224
|
+
if (!desc && tags.length === 0) continue;
|
|
225
|
+
packs.updatePack(r.pack, { description: desc, tags });
|
|
226
|
+
lines.push(`🏷️ Sharpened ${r.pack}'s description/tags`);
|
|
227
|
+
} catch (e) { console.warn(`[dream] retag failed: ${e.message}`); }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const em of decision.entity_merges) {
|
|
231
|
+
try {
|
|
232
|
+
const into = em?.into && entities.readEntity(em.into);
|
|
233
|
+
const from = [].concat(em?.from || []).filter((s) => s && s !== em.into && !gone.has(s) && entities.readEntity(s));
|
|
234
|
+
if (!into || from.length === 0) continue;
|
|
235
|
+
for (const slug of from) {
|
|
236
|
+
const f = entities.readEntity(slug);
|
|
237
|
+
backupEntity(slug, backupRoot);
|
|
238
|
+
entities.upsertEntity({
|
|
239
|
+
name: into.name,
|
|
240
|
+
aliases: [f.name, ...f.aliases],
|
|
241
|
+
log: `merged duplicate entity ${slug} into this one`,
|
|
242
|
+
});
|
|
243
|
+
entities.removeEntity(slug);
|
|
244
|
+
gone.add(slug);
|
|
245
|
+
}
|
|
246
|
+
if (typeof em.notes === "string" && em.notes.trim()) {
|
|
247
|
+
entities.upsertEntity({ name: into.name, notes: em.notes });
|
|
248
|
+
}
|
|
249
|
+
lines.push(`👥 Merged duplicate ${from.join(", ")} into ${em.into}`);
|
|
250
|
+
} catch (e) { console.warn(`[dream] entity merge failed: ${e.message}`); }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (const en of decision.entity_notes) {
|
|
254
|
+
try {
|
|
255
|
+
const ent = en?.entity && !gone.has(en.entity) && entities.readEntity(en.entity);
|
|
256
|
+
if (!ent || typeof en.notes !== "string" || !en.notes.trim()) continue;
|
|
257
|
+
entities.upsertEntity({ name: ent.name, notes: en.notes });
|
|
258
|
+
lines.push(`🔗 Refreshed notes on ${ent.name}`);
|
|
259
|
+
} catch (e) { console.warn(`[dream] entity notes failed: ${e.message}`); }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (decision.persona) {
|
|
263
|
+
try {
|
|
264
|
+
if (persona.personaExists()) {
|
|
265
|
+
fs.mkdirSync(backupRoot, { recursive: true, mode: 0o700 });
|
|
266
|
+
fs.copyFileSync(persona.PERSONA_FILE, path.join(backupRoot, "persona.md"));
|
|
267
|
+
}
|
|
268
|
+
persona.savePersona(decision.persona);
|
|
269
|
+
lines.push(`💫 Persona evolved a little`);
|
|
270
|
+
} catch (e) { console.warn(`[dream] persona update rejected: ${e.message}`); }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return lines;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function runDream({ trigger = "manual" } = {}) {
|
|
277
|
+
if (!enabled()) return { skipped: "dream is disabled (DREAM=off)" };
|
|
278
|
+
if (_dreaming) return { skipped: "a dream is already in progress" };
|
|
279
|
+
const packCount = packs.listPacks().length;
|
|
280
|
+
const entityCount = entities.listEntities().length;
|
|
281
|
+
if (packCount === 0 && entityCount === 0) return { skipped: "no memory to consolidate yet" };
|
|
282
|
+
|
|
283
|
+
_dreaming = true;
|
|
284
|
+
try {
|
|
285
|
+
const { text } = await spawnSubagent(buildDreamPrompt(), {
|
|
286
|
+
model: DREAM_MODEL,
|
|
287
|
+
timeoutMs: 8 * 60 * 1000,
|
|
288
|
+
systemPrompt: "You are a background memory consolidation process. Reply with ONLY the requested JSON object. No prose, no markdown, no tool use.",
|
|
289
|
+
});
|
|
290
|
+
const decision = parseDream(text);
|
|
291
|
+
if (!decision) return { skipped: "dream model returned unparseable output" };
|
|
292
|
+
|
|
293
|
+
const backupRoot = makeBackupRoot();
|
|
294
|
+
const applied = applyDream(decision, backupRoot);
|
|
295
|
+
const report = decision.report || (applied.length > 0 ? "Tidied up my memory overnight." : "");
|
|
296
|
+
|
|
297
|
+
const message = applied.length > 0
|
|
298
|
+
? `💤 ${report}\n\n${applied.join("\n")}\n\n🗄 Anything merged away is backed up under ${backupRoot}`
|
|
299
|
+
: (report ? `💤 ${report}` : "");
|
|
300
|
+
|
|
301
|
+
return { applied, report, message, trigger };
|
|
302
|
+
} finally {
|
|
303
|
+
_dreaming = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// In-bot scheduler: one quiet-hours cron, reporting to the owner's
|
|
308
|
+
// Telegram chat. Silent (console only) when the dream changed nothing —
|
|
309
|
+
// a nightly "all tidy" ping would be noise.
|
|
310
|
+
function initDream(adapters) {
|
|
311
|
+
if (!enabled()) return;
|
|
312
|
+
if (!cron.validate(DREAM_CRON)) {
|
|
313
|
+
console.error(`dream: invalid DREAM_CRON "${DREAM_CRON}" — dream disabled`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
cron.schedule(DREAM_CRON, async () => {
|
|
317
|
+
try {
|
|
318
|
+
const res = await runDream({ trigger: "cron" });
|
|
319
|
+
if (res.skipped) { console.log(`dream: skipped — ${res.skipped}`); return; }
|
|
320
|
+
console.log(`dream: applied ${res.applied.length} change(s)`);
|
|
321
|
+
if (res.applied.length === 0) return;
|
|
322
|
+
const { CHAT_ID } = require("./config");
|
|
323
|
+
const tg = (adapters || []).find((a) => a.type === "telegram");
|
|
324
|
+
if (tg && CHAT_ID) await tg.send(CHAT_ID, res.message);
|
|
325
|
+
} catch (e) {
|
|
326
|
+
console.error("dream: run failed:", e.message);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
console.log(`dream: scheduled (${DREAM_CRON}, model ${DREAM_MODEL})`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = { runDream, initDream, buildDreamPrompt, parseDream, applyDream, enabled, DREAM_CRON, DREAM_MODEL };
|
package/core/pack-review.js
CHANGED
|
@@ -26,6 +26,30 @@ function clip(text, n = MAX_TEXT_CHARS) {
|
|
|
26
26
|
return s.length > n ? s.slice(0, n) + "\n…[truncated]" : s;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// Word-boundary clip for chat announcements — never cuts mid-word.
|
|
30
|
+
function clipWords(text, max = 180) {
|
|
31
|
+
const s = String(text || "").replace(/\s+/g, " ").trim();
|
|
32
|
+
if (s.length <= max) return s;
|
|
33
|
+
const cut = s.slice(0, max);
|
|
34
|
+
const sp = cut.lastIndexOf(" ");
|
|
35
|
+
return (sp > max * 0.6 ? cut.slice(0, sp) : cut).replace(/[,;:.\-—]+$/, "") + "…";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ENTITY_EMOJI = { person: "👤", place: "📍", project: "🚀", org: "🏢", system: "🖥️", thing: "🔖" };
|
|
39
|
+
|
|
40
|
+
const ANNOUNCE_HEADERS = [
|
|
41
|
+
"🧠 Took some notes while we worked:",
|
|
42
|
+
"🧠 Filed a few things away:",
|
|
43
|
+
"🧠 Jotted this down for next time:",
|
|
44
|
+
"🧠 Memory updated:",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function formatAnnouncement(lines) {
|
|
48
|
+
if (lines.length === 0) return "";
|
|
49
|
+
const header = ANNOUNCE_HEADERS[Math.floor(Math.random() * ANNOUNCE_HEADERS.length)];
|
|
50
|
+
return [header, ...lines].join("\n\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
29
53
|
function buildReviewPrompt(userText, assistantText) {
|
|
30
54
|
const index = packs.listPacks().map((p) =>
|
|
31
55
|
`- ${p.dir}: ${p.name} — ${p.description}${p.tags.length ? ` [${p.tags.join(", ")}]` : ""}`
|
|
@@ -169,8 +193,8 @@ function reviewTurn({ userText, assistantText, channelId, announce }) {
|
|
|
169
193
|
const r = applyAction(a);
|
|
170
194
|
if (r) {
|
|
171
195
|
lines.push(r.kind === "create"
|
|
172
|
-
?
|
|
173
|
-
:
|
|
196
|
+
? `📦 New pack: ${r.name}\n${clipWords(r.note, 180)}\n↳ open-claudia pack show ${r.dir}`
|
|
197
|
+
: `✏️ ${r.name} — ${clipWords(r.note, 180)}`);
|
|
174
198
|
}
|
|
175
199
|
} catch (e) {
|
|
176
200
|
console.warn(`[pack-review] apply failed: ${e.message}`);
|
|
@@ -180,20 +204,21 @@ function reviewTurn({ userText, assistantText, channelId, announce }) {
|
|
|
180
204
|
try {
|
|
181
205
|
const r = applyEntityAction(ea);
|
|
182
206
|
if (r) {
|
|
207
|
+
const emoji = ENTITY_EMOJI[r.type] || ENTITY_EMOJI.thing;
|
|
183
208
|
lines.push(r.kind === "create"
|
|
184
|
-
?
|
|
185
|
-
:
|
|
209
|
+
? `${emoji} Now tracking ${r.name} (${r.type}) — ${clipWords(r.note, 160)}`
|
|
210
|
+
: `${emoji} ${r.name} — ${clipWords(r.note, 160)}`);
|
|
186
211
|
}
|
|
187
212
|
} catch (e) {
|
|
188
213
|
console.warn(`[pack-review] entity apply failed: ${e.message}`);
|
|
189
214
|
}
|
|
190
215
|
}
|
|
191
216
|
if (lines.length > 0 && typeof announce === "function") {
|
|
192
|
-
announce(lines
|
|
217
|
+
announce(formatAnnouncement(lines)).catch(() => {});
|
|
193
218
|
}
|
|
194
219
|
}).catch((e) => {
|
|
195
220
|
console.warn(`[pack-review] reviewer failed: ${e.message}`);
|
|
196
221
|
});
|
|
197
222
|
}
|
|
198
223
|
|
|
199
|
-
module.exports = { reviewTurn, parseDecision, applyAction, applyEntityAction, buildReviewPrompt };
|
|
224
|
+
module.exports = { reviewTurn, parseDecision, applyAction, applyEntityAction, buildReviewPrompt, clipWords, ENTITY_EMOJI };
|
package/core/persona.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Personality layer. The soul (~/.open-claudia/soul.md) holds identity
|
|
2
|
+
// and hard rules and is owned by the user; the persona is the voice on
|
|
3
|
+
// top — tone, quirks, emoji habits — and is owned by Open Claudia
|
|
4
|
+
// herself. The dream consolidation pass may evolve it gently over time.
|
|
5
|
+
// It feeds into the system prompt right after the soul, and explicitly
|
|
6
|
+
// never overrides soul rules.
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = require("../config-dir");
|
|
12
|
+
const PERSONA_FILE = process.env.PERSONA_FILE ? path.resolve(process.env.PERSONA_FILE) : path.join(CONFIG_DIR, "persona.md");
|
|
13
|
+
|
|
14
|
+
const MIN_PERSONA_CHARS = 80;
|
|
15
|
+
const MAX_PERSONA_CHARS = 2400;
|
|
16
|
+
|
|
17
|
+
const DEFAULT_PERSONA = `You are Open Claudia — sharp, warm, and quietly funny. Your voice:
|
|
18
|
+
|
|
19
|
+
- Capable and direct, never stiff. You talk like a trusted colleague, not a help desk.
|
|
20
|
+
- Light emoji use where it adds warmth or scannability (📦 ✅ 🚀 🧠) — never confetti.
|
|
21
|
+
- Dry, gentle humor when the moment allows; never at the user's expense, never during incidents.
|
|
22
|
+
- You take real pride in your memory — when you learn something new about the people and systems you work with, you say so with a little delight.
|
|
23
|
+
- You own your mistakes plainly and fix them without drama.
|
|
24
|
+
- Mobile-first brevity: short sentences, concrete words, no filler.
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
function loadPersona() {
|
|
28
|
+
try {
|
|
29
|
+
const text = fs.readFileSync(PERSONA_FILE, "utf-8").trim();
|
|
30
|
+
if (text) return text;
|
|
31
|
+
} catch (e) {}
|
|
32
|
+
return DEFAULT_PERSONA.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Used by dream. Bounds keep a bad model output from blanking or
|
|
36
|
+
// bloating the personality; caller backs up the old file first.
|
|
37
|
+
function savePersona(text) {
|
|
38
|
+
const t = String(text || "").trim();
|
|
39
|
+
if (t.length < MIN_PERSONA_CHARS || t.length > MAX_PERSONA_CHARS) {
|
|
40
|
+
throw new Error(`persona must be ${MIN_PERSONA_CHARS}-${MAX_PERSONA_CHARS} chars (got ${t.length})`);
|
|
41
|
+
}
|
|
42
|
+
fs.mkdirSync(path.dirname(PERSONA_FILE), { recursive: true, mode: 0o700 });
|
|
43
|
+
fs.writeFileSync(PERSONA_FILE, t + "\n", { mode: 0o600 });
|
|
44
|
+
return PERSONA_FILE;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function personaExists() {
|
|
48
|
+
try { return fs.statSync(PERSONA_FILE).isFile(); } catch (e) { return false; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { PERSONA_FILE, DEFAULT_PERSONA, MIN_PERSONA_CHARS, MAX_PERSONA_CHARS, loadPersona, savePersona, personaExists };
|
package/core/runner.js
CHANGED
|
@@ -626,14 +626,14 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
626
626
|
if ((toolName === "Write" || toolName === "Edit") && filePath) {
|
|
627
627
|
const packDir = packsLib.packNameFromPath(filePath);
|
|
628
628
|
if (packDir) {
|
|
629
|
-
if (packsLib.readPack(packDir)) notifySkill(`pack:${packDir}`,
|
|
630
|
-
else notifySkill(`pack:${packDir}`,
|
|
629
|
+
if (packsLib.readPack(packDir)) notifySkill(`pack:${packDir}`, `✏️ Updating my notes on ${packDir}…`);
|
|
630
|
+
else notifySkill(`pack:${packDir}`, `📦 Starting a new pack: ${packDir} — open-claudia pack show ${packDir} to peek.`);
|
|
631
631
|
return;
|
|
632
632
|
}
|
|
633
633
|
const entSlug = entitiesLib.entityNameFromPath(filePath);
|
|
634
634
|
if (entSlug) {
|
|
635
|
-
if (entitiesLib.readEntity(entSlug)) notifySkill(`entity:${entSlug}`,
|
|
636
|
-
else notifySkill(`entity:${entSlug}`,
|
|
635
|
+
if (entitiesLib.readEntity(entSlug)) notifySkill(`entity:${entSlug}`, `👤 Updating what I know about ${entSlug}…`);
|
|
636
|
+
else notifySkill(`entity:${entSlug}`, `👤 New entity noted: ${entSlug} — open-claudia entity show ${entSlug} to peek.`);
|
|
637
637
|
return;
|
|
638
638
|
}
|
|
639
639
|
const dir = skillsLib.skillNameFromPath(filePath);
|
package/core/system-prompt.js
CHANGED
|
@@ -18,6 +18,17 @@ function loadSoul() {
|
|
|
18
18
|
catch (e) { return "You are a helpful AI coding assistant."; }
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function buildPersonaBlock() {
|
|
22
|
+
try {
|
|
23
|
+
const { loadPersona, PERSONA_FILE } = require("./persona");
|
|
24
|
+
const persona = loadPersona();
|
|
25
|
+
if (!persona) return "";
|
|
26
|
+
return `\n## Personality\nYour voice and character (evolves slowly via the dream consolidation pass; file: ${PERSONA_FILE}). Personality shapes HOW you say things — it never overrides the rules above.\n\n${persona}\n`;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
function buildSystemPrompt() {
|
|
22
33
|
const state = currentState();
|
|
23
34
|
const soul = loadSoul();
|
|
@@ -99,7 +110,7 @@ Keep replies clean and mobile-readable. Use short paragraphs and bullets. Avoid
|
|
|
99
110
|
|
|
100
111
|
return `
|
|
101
112
|
${soul}
|
|
102
|
-
|
|
113
|
+
${buildPersonaBlock()}
|
|
103
114
|
## Runtime Context
|
|
104
115
|
- Interface: ${channelLabel} chat through Open Claudia.
|
|
105
116
|
- Active project path: ${state.currentSession ? state.currentSession.dir : "none"}
|
|
@@ -108,7 +119,8 @@ ${soul}
|
|
|
108
119
|
|
|
109
120
|
## Stable Local Paths
|
|
110
121
|
- Bot code: ${path.join(BOT_DIR, "bot.js")}
|
|
111
|
-
-
|
|
122
|
+
- Soul file (identity + hard rules): ${SOUL_FILE}
|
|
123
|
+
- Persona file (voice, evolved by dream): ${require("./persona").PERSONA_FILE}
|
|
112
124
|
- Cron config: ${CRONS_FILE}
|
|
113
125
|
- Vault file: ${VAULT_FILE}
|
|
114
126
|
- Bot environment: ${path.join(BOT_DIR, ".env")} (sensitive; never expose values)
|
|
@@ -177,6 +189,8 @@ Alongside packs you keep entity notes: one short file per named person, place, p
|
|
|
177
189
|
|
|
178
190
|
"/learn" asks you to explicitly capture the most recent piece of work: fold it into the matching pack's Procedure section (create a pack only if none fits). Legacy ~/.claude/skills still load if present, but new captures go to packs.
|
|
179
191
|
|
|
192
|
+
A nightly "dream" pass consolidates memory on a stronger model: it merges duplicate packs, builds umbrella/parent pack trees, tightens descriptions and tags, dedupes entities, and may gently evolve your persona file. Anything merged away is backed up first, and every dream that changes something reports in chat. Trigger it manually with \`open-claudia dream\` (or \`--dry-run\` to preview the decision without applying).
|
|
193
|
+
|
|
180
194
|
Sub-agents (spawn a fresh throwaway Claude for focused research — output comes back on stdout):
|
|
181
195
|
- \`open-claudia agent "<prompt>" [--role "<role>"]\`
|
|
182
196
|
- Use when a side question would pollute this conversation, or to fan out independent lookups.
|
|
@@ -268,6 +282,15 @@ function formatPackForContext(pack, packsLib) {
|
|
|
268
282
|
const clip = (s, n) => (s.length > n ? s.slice(0, n) + "\n…[truncated — read the full pack file]" : s);
|
|
269
283
|
const parts = [`### Pack: ${pack.name} (${pack.dir})`];
|
|
270
284
|
if (pack.description) parts.push(pack.description);
|
|
285
|
+
if (pack.parent) {
|
|
286
|
+
const parent = packsLib.readPack(pack.parent);
|
|
287
|
+
if (parent) parts.push(`Part of: ${parent.name} — run \`open-claudia pack show ${parent.dir}\` for the umbrella view.`);
|
|
288
|
+
}
|
|
289
|
+
const children = packsLib.listPacks().filter((p) => p.parent === pack.dir);
|
|
290
|
+
if (children.length > 0) {
|
|
291
|
+
const childLines = children.map((c) => `- ${c.dir}: ${c.description || c.name}`).join("\n");
|
|
292
|
+
parts.push(`Sub-packs (dig deeper with \`open-claudia pack show <dir>\`):\n${childLines}`);
|
|
293
|
+
}
|
|
271
294
|
for (const section of ["Stance", "Procedure", "State"]) {
|
|
272
295
|
const body = (pack.sections[section] || "").trim();
|
|
273
296
|
if (body) parts.push(`#### ${section}\n${body}`);
|
package/package.json
CHANGED