memoir-cli 3.3.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -195
- package/bin/memoir.js +31 -28
- package/package.json +4 -3
- package/src/cloud/constants.js +1 -1
- package/src/commands/activate.js +232 -0
- package/src/commands/push.js +23 -8
- package/src/commands/restore.js +14 -8
- package/src/commands/upgrade.js +1 -1
- package/src/config.js +45 -0
package/README.md
CHANGED
|
@@ -2,37 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
# memoir
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Portable memory for every AI coding tool.**
|
|
6
6
|
|
|
7
7
|
[](https://npmjs.org/package/memoir-cli)
|
|
8
8
|
[](https://npmjs.org/package/memoir-cli)
|
|
9
9
|
[](https://github.com/camgitt/memoir/stargazers)
|
|
10
10
|
[](LICENSE)
|
|
11
|
-
[](https://nodejs.org)
|
|
12
|
-
|
|
13
|
-
Your AI forgets everything between sessions. memoir gives it long-term memory via MCP.
|
|
14
|
-
|
|
15
|
-
**11 tools supported** • **E2E encrypted** • **Cross-platform** • **Open source**
|
|
16
|
-
|
|
17
|
-
Works with Claude Code, Cursor, Windsurf, Gemini, Copilot, Codex, ChatGPT, Aider, Zed, Cline, and Continue.dev.
|
|
18
|
-
|
|
19
|
-
[Website](https://memoir.sh) • [npm](https://npmjs.org/package/memoir-cli) • [Blog](https://memoir.sh/blog) • [Contributing](CONTRIBUTING.md)
|
|
20
|
-
|
|
21
|
-
<br />
|
|
22
|
-
|
|
23
|
-
<img src="demo.svg" alt="memoir demo — push, restore, and sync AI memory" width="700" />
|
|
24
11
|
|
|
25
12
|
</div>
|
|
26
13
|
|
|
27
|
-
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g memoir-cli
|
|
16
|
+
memoir activate
|
|
17
|
+
```
|
|
28
18
|
|
|
29
|
-
|
|
19
|
+
Your AI now remembers across sessions, tools, and machines. Works with Claude Code, Cursor, Windsurf, Gemini, Copilot, and 6 more tools.
|
|
30
20
|
|
|
31
|
-
|
|
21
|
+
---
|
|
32
22
|
|
|
33
|
-
##
|
|
23
|
+
## What it does
|
|
34
24
|
|
|
35
|
-
memoir is
|
|
25
|
+
memoir is an [MCP server](https://modelcontextprotocol.io) that gives your AI tools persistent memory. Your AI can search, save, and recall context automatically.
|
|
36
26
|
|
|
37
27
|
```
|
|
38
28
|
you: how does auth work in this project?
|
|
@@ -47,38 +37,15 @@ claude: Based on your previous sessions: this project uses JWT auth
|
|
|
47
37
|
|
|
48
38
|
No re-explaining. memoir remembered.
|
|
49
39
|
|
|
50
|
-
##
|
|
51
|
-
|
|
52
|
-
```mermaid
|
|
53
|
-
graph LR
|
|
54
|
-
A[Claude Code] --> M[memoir MCP]
|
|
55
|
-
B[Cursor] --> M
|
|
56
|
-
C[Gemini CLI] --> M
|
|
57
|
-
D[Windsurf] --> M
|
|
58
|
-
E[+ 7 more] --> M
|
|
59
|
-
|
|
60
|
-
M --> R[recall / remember / list / read]
|
|
61
|
-
R --> S[(Local Memory Store)]
|
|
62
|
-
|
|
63
|
-
S --> P[memoir push]
|
|
64
|
-
P --> G[GitHub / Cloud]
|
|
65
|
-
G --> Q[memoir restore]
|
|
66
|
-
Q --> S2[(New Machine)]
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
memoir runs as a local MCP server inside your AI tools. Your AI can search, save, and recall memories automatically. Push to sync across machines.
|
|
40
|
+
## Setup
|
|
70
41
|
|
|
71
|
-
|
|
42
|
+
### 1. Install
|
|
72
43
|
|
|
73
44
|
```bash
|
|
74
|
-
# Install
|
|
75
45
|
npm install -g memoir-cli
|
|
76
|
-
|
|
77
|
-
# Setup
|
|
78
|
-
memoir init
|
|
79
46
|
```
|
|
80
47
|
|
|
81
|
-
### Add MCP to your AI
|
|
48
|
+
### 2. Add MCP to your AI tool
|
|
82
49
|
|
|
83
50
|
**Claude Code** — add to `~/.mcp.json`:
|
|
84
51
|
```json
|
|
@@ -98,7 +65,13 @@ memoir init
|
|
|
98
65
|
}
|
|
99
66
|
```
|
|
100
67
|
|
|
101
|
-
|
|
68
|
+
### 3. Activate in your project
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
memoir activate
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That's it. Your AI now has 6 memory tools:
|
|
102
75
|
|
|
103
76
|
| MCP Tool | What it does |
|
|
104
77
|
|----------|-------------|
|
|
@@ -109,49 +82,28 @@ That's it. Your AI tools now have 6 memory abilities:
|
|
|
109
82
|
| `memoir_status` | See which AI tools are detected |
|
|
110
83
|
| `memoir_profiles` | Switch between work/personal |
|
|
111
84
|
|
|
112
|
-
##
|
|
113
|
-
|
|
114
|
-
memoir syncs three layers that no other tool connects:
|
|
85
|
+
## Why memoir
|
|
115
86
|
|
|
116
|
-
|
|
117
|
-
Configs, instructions, and project knowledge across 11 tools — searchable and writable from inside any AI conversation.
|
|
87
|
+
Your AI forgets everything between sessions. You re-explain your codebase, your conventions, your decisions — every time.
|
|
118
88
|
|
|
119
|
-
|
|
120
|
-
|------|-----------------|
|
|
121
|
-
| **Claude Code** | ~/.claude/ settings, memory, CLAUDE.md files |
|
|
122
|
-
| **Gemini CLI** | ~/.gemini/ config, GEMINI.md files |
|
|
123
|
-
| **ChatGPT** | CHATGPT.md custom instructions |
|
|
124
|
-
| **OpenAI Codex** | ~/.codex/ config, AGENTS.md |
|
|
125
|
-
| **Cursor** | Settings, keybindings, .cursorrules |
|
|
126
|
-
| **GitHub Copilot** | Config, copilot-instructions.md |
|
|
127
|
-
| **Windsurf** | Settings, keybindings, .windsurfrules |
|
|
128
|
-
| **Zed** | Settings, keymap, tasks |
|
|
129
|
-
| **Cline** | Settings, .clinerules |
|
|
130
|
-
| **Continue.dev** | Config, .continuerules |
|
|
131
|
-
| **Aider** | .aider.conf.yml, system prompt |
|
|
89
|
+
memoir fixes this by giving your AI a shared memory layer that works across **every tool you use**. Tell Claude something once. Cursor knows it too.
|
|
132
90
|
|
|
133
|
-
|
|
134
|
-
What you were **doing** — not just what your AI knows, but the active context.
|
|
91
|
+
**11 tools supported:** Claude Code, Cursor, Windsurf, Gemini CLI, GitHub Copilot, OpenAI Codex, ChatGPT, Aider, Zed, Cline, Continue.dev
|
|
135
92
|
|
|
136
|
-
|
|
137
|
-
- What files you changed, what errors you hit, what decisions you made
|
|
138
|
-
- Injected into your AI on restore so it picks up mid-conversation
|
|
139
|
-
- Secrets auto-redacted (API keys, tokens, passwords stripped before sync)
|
|
93
|
+
## Sync across machines
|
|
140
94
|
|
|
141
|
-
|
|
142
|
-
|
|
95
|
+
```bash
|
|
96
|
+
# Back up
|
|
97
|
+
memoir push
|
|
143
98
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
- **Zero git commands needed** — memoir handles it all
|
|
99
|
+
# Restore on any machine
|
|
100
|
+
memoir restore -y
|
|
101
|
+
```
|
|
148
102
|
|
|
149
|
-
|
|
103
|
+
Push syncs AI memory, session context, workspace (git repos + uncommitted work), and project configs. E2E encrypted with AES-256-GCM.
|
|
150
104
|
|
|
151
|
-
|
|
152
|
-
Tell Claude something once. Cursor knows it too. memoir is the shared memory layer between all your AI tools.
|
|
105
|
+
## Translate between AI tools
|
|
153
106
|
|
|
154
|
-
### Translate between AI tools
|
|
155
107
|
```bash
|
|
156
108
|
memoir migrate --from chatgpt --to claude
|
|
157
109
|
# AI-powered — rewrites conventions, not copy-paste
|
|
@@ -160,56 +112,25 @@ memoir migrate --from chatgpt --to all
|
|
|
160
112
|
# Translate to every tool at once
|
|
161
113
|
```
|
|
162
114
|
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
# On your main machine
|
|
166
|
-
memoir push
|
|
167
|
-
|
|
168
|
-
# On any other machine
|
|
169
|
-
memoir restore -y
|
|
170
|
-
# ✔ AI memory restored (Claude, Gemini, Cursor, 11 tools)
|
|
171
|
-
# ✔ 44 projects cloned & unpacked
|
|
172
|
-
# ✔ Uncommitted changes applied
|
|
173
|
-
# ✔ Session context injected — AI picks up mid-conversation
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### E2E Encryption
|
|
177
|
-
```bash
|
|
178
|
-
memoir encrypt # toggle encryption on/off
|
|
179
|
-
memoir push # prompted for passphrase, AES-256-GCM encrypted
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
Your backup is encrypted before it leaves your machine. Secret scanning auto-redacts API keys, tokens, and passwords.
|
|
183
|
-
|
|
184
|
-
### Profiles (personal / work)
|
|
185
|
-
```bash
|
|
186
|
-
memoir profile create work
|
|
187
|
-
memoir push --profile work
|
|
188
|
-
memoir profile switch personal
|
|
189
|
-
```
|
|
115
|
+
## Cloud sync
|
|
190
116
|
|
|
191
|
-
### Cloud sync
|
|
192
117
|
```bash
|
|
193
118
|
memoir login
|
|
194
119
|
memoir cloud push # encrypted cloud backup
|
|
195
120
|
memoir cloud restore # restore from any version
|
|
121
|
+
memoir history # view backup versions
|
|
122
|
+
memoir share # create encrypted shareable link
|
|
196
123
|
```
|
|
197
124
|
|
|
198
|
-
### Cross-platform (Mac / Windows / Linux)
|
|
199
|
-
Paths remap automatically between platforms. Push from Mac, restore on Windows. It just works.
|
|
200
|
-
|
|
201
125
|
## All Commands
|
|
202
126
|
|
|
203
127
|
| Command | What it does |
|
|
204
128
|
|---------|-------------|
|
|
205
|
-
| `memoir
|
|
129
|
+
| `memoir activate` | Enable auto-recall in this project |
|
|
130
|
+
| `memoir deactivate` | Remove memoir from this project |
|
|
206
131
|
| `memoir push` | Back up AI memory + workspace + session |
|
|
207
132
|
| `memoir restore` | Restore everything on a new machine |
|
|
208
|
-
| `memoir mcp` | Start MCP server for editor integration |
|
|
209
133
|
| `memoir status` | Show detected AI tools |
|
|
210
|
-
| `memoir doctor` | Diagnose issues, scan for secrets |
|
|
211
|
-
| `memoir view` | Preview what's in your backup |
|
|
212
|
-
| `memoir diff` | Show changes since last backup |
|
|
213
134
|
| `memoir migrate` | Translate memory between tools via AI |
|
|
214
135
|
| `memoir snapshot` | Capture current coding session |
|
|
215
136
|
| `memoir resume` | Pick up where you left off |
|
|
@@ -217,95 +138,24 @@ Paths remap automatically between platforms. Push from Mac, restore on Windows.
|
|
|
217
138
|
| `memoir profile` | Manage profiles (personal/work) |
|
|
218
139
|
| `memoir cloud push` | Back up to memoir cloud |
|
|
219
140
|
| `memoir cloud restore` | Restore from memoir cloud |
|
|
220
|
-
| `memoir history` | View cloud backup versions |
|
|
221
|
-
| `memoir login` | Sign in to memoir cloud |
|
|
222
141
|
| `memoir share` | Create encrypted shareable link |
|
|
223
|
-
| `memoir
|
|
142
|
+
| `memoir doctor` | Diagnose issues |
|
|
143
|
+
| `memoir diff` | Show changes since last backup |
|
|
144
|
+
| `memoir view` | Preview what's in your backup |
|
|
224
145
|
| `memoir update` | Self-update to latest version |
|
|
225
146
|
|
|
226
|
-
## How memoir compares
|
|
227
|
-
|
|
228
|
-
| Feature | memoir | dotfiles managers | ai-rulez | memories.sh |
|
|
229
|
-
|---------|--------|-------------------|----------|-------------|
|
|
230
|
-
| MCP memory layer | **6 tools** | No | No | No |
|
|
231
|
-
| AI memory sync | **11 tools** | No | 18 tools | 3 tools |
|
|
232
|
-
| Cross-tool recall | **Yes** | No | No | No |
|
|
233
|
-
| Workspace sync | **Yes** | No | No | No |
|
|
234
|
-
| Session handoff | **Yes** | No | No | No |
|
|
235
|
-
| AI-powered migration | **Yes** | No | No | No |
|
|
236
|
-
| E2E encryption | **Yes** | No | No | No |
|
|
237
|
-
| Secret scanning | **Yes** | Some | No | No |
|
|
238
|
-
| Cross-platform remap | **Yes** | Some | No | No |
|
|
239
|
-
| Cloud backup | **Yes** | No | No | Yes ($15/mo) |
|
|
240
|
-
| Profiles | **Yes** | No | No | No |
|
|
241
|
-
| Free & open source | **Yes** | Yes | Yes | No |
|
|
242
|
-
|
|
243
|
-
## Common Workflows
|
|
244
|
-
|
|
245
|
-
### Your AI remembers across sessions
|
|
246
|
-
```
|
|
247
|
-
# Monday — you explain your auth setup to Claude
|
|
248
|
-
# ...Claude calls memoir_remember to save the decision
|
|
249
|
-
|
|
250
|
-
# Thursday — new conversation
|
|
251
|
-
you: "add a protected route"
|
|
252
|
-
# Claude calls memoir_recall, finds your auth architecture
|
|
253
|
-
# No re-explaining needed
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
### New machine setup
|
|
257
|
-
```bash
|
|
258
|
-
npm install -g memoir-cli && memoir init && memoir restore -y
|
|
259
|
-
# Done. Everything's back.
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
### Switching AI tools
|
|
263
|
-
```bash
|
|
264
|
-
memoir migrate --from chatgpt --to claude
|
|
265
|
-
# Your custom instructions become a proper CLAUDE.md
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
### Team onboarding
|
|
269
|
-
```bash
|
|
270
|
-
# Senior dev pushes team config
|
|
271
|
-
memoir push --profile team
|
|
272
|
-
|
|
273
|
-
# New hire runs one command
|
|
274
|
-
memoir restore --profile team
|
|
275
|
-
# Every project cloned. Every AI tool configured. Day one productive.
|
|
276
|
-
```
|
|
277
|
-
|
|
278
147
|
## Security
|
|
279
148
|
|
|
280
149
|
- **E2E encryption** — AES-256-GCM with scrypt key derivation
|
|
281
|
-
- **Secret scanning** —
|
|
282
|
-
- **
|
|
283
|
-
- **
|
|
284
|
-
- **Passphrase verified** — wrong passphrase caught before decrypt attempt
|
|
285
|
-
- **Local MCP server** — runs on your machine, no data sent to external services
|
|
286
|
-
|
|
287
|
-
## Requirements
|
|
288
|
-
|
|
289
|
-
- Node.js >= 18
|
|
290
|
-
- Git (for workspace sync)
|
|
291
|
-
- Works on macOS, Windows, Linux
|
|
292
|
-
|
|
293
|
-
## Contributing
|
|
294
|
-
|
|
295
|
-
Contributions welcome — especially new tool adapters and MCP improvements.
|
|
296
|
-
|
|
297
|
-
1. Fork the repo
|
|
298
|
-
2. Create your branch (`git checkout -b feature/my-feature`)
|
|
299
|
-
3. Commit and push
|
|
300
|
-
4. Open a PR
|
|
150
|
+
- **Secret scanning** — API keys, tokens, passwords auto-redacted before sync
|
|
151
|
+
- **Local MCP server** — runs on your machine, no data sent externally
|
|
152
|
+
- **Zero-knowledge cloud** — encrypted before upload
|
|
301
153
|
|
|
302
154
|
## Links
|
|
303
155
|
|
|
304
156
|
- **Website:** [memoir.sh](https://memoir.sh)
|
|
305
157
|
- **npm:** [memoir-cli](https://npmjs.org/package/memoir-cli)
|
|
306
|
-
- **Blog:** [memoir.sh/blog](https://memoir.sh/blog)
|
|
307
158
|
- **Issues:** [GitHub Issues](https://github.com/camgitt/memoir/issues)
|
|
159
|
+
- **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
308
160
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
MIT
|
|
161
|
+
MIT Licensed
|
package/bin/memoir.js
CHANGED
|
@@ -20,6 +20,7 @@ import { shareCommand } from '../src/commands/share.js';
|
|
|
20
20
|
import { historyCommand } from '../src/commands/history.js';
|
|
21
21
|
import { projectsListCommand, projectsTodoCommand } from '../src/commands/projects.js';
|
|
22
22
|
import { upgradeCommand } from '../src/commands/upgrade.js';
|
|
23
|
+
import { activateCommand, deactivateCommand } from '../src/commands/activate.js';
|
|
23
24
|
import { createRequire } from 'module';
|
|
24
25
|
|
|
25
26
|
const require = createRequire(import.meta.url);
|
|
@@ -52,40 +53,18 @@ async function checkForUpdate() {
|
|
|
52
53
|
} catch {}
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
//
|
|
56
|
+
// When run with no args: auto-push (zero-config)
|
|
56
57
|
if (process.argv.length <= 2) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
chalk.gray(' Your AI remembers everything.') + '\n\n' +
|
|
60
|
-
chalk.white.bold('Quick Start:') + '\n' +
|
|
61
|
-
chalk.cyan(' memoir init ') + chalk.gray('— first-time setup') + '\n' +
|
|
62
|
-
chalk.cyan(' memoir push ') + chalk.gray('— back up your AI memory') + '\n' +
|
|
63
|
-
chalk.cyan(' memoir restore ') + chalk.gray('— restore on a new machine') + '\n' +
|
|
64
|
-
chalk.cyan(' memoir snapshot ') + chalk.gray('— capture your current session') + '\n' +
|
|
65
|
-
chalk.cyan(' memoir resume ') + chalk.gray('— pick up where you left off') + '\n' +
|
|
66
|
-
chalk.cyan(' memoir status ') + chalk.gray('— see detected AI tools') + '\n' +
|
|
67
|
-
chalk.cyan(' memoir profile ') + chalk.gray('— manage profiles (personal/work)') + '\n' +
|
|
68
|
-
chalk.cyan(' memoir projects ') + chalk.gray('— see all your projects at a glance') + '\n' +
|
|
69
|
-
chalk.cyan(' memoir encrypt ') + chalk.gray('— toggle E2E encryption') + '\n' +
|
|
70
|
-
chalk.cyan(' memoir update ') + chalk.gray('— update to latest version') + '\n' +
|
|
71
|
-
chalk.cyan(' memoir upgrade ') + chalk.gray('— view plans & upgrade') + '\n\n' +
|
|
72
|
-
chalk.white.bold('Cloud (Pro):') + '\n' +
|
|
73
|
-
chalk.cyan(' memoir login ') + chalk.gray('— sign in to memoir cloud') + '\n' +
|
|
74
|
-
chalk.cyan(' memoir cloud push ') + chalk.gray('— back up to the cloud') + '\n' +
|
|
75
|
-
chalk.cyan(' memoir cloud restore ') + chalk.gray('— restore from cloud') + '\n' +
|
|
76
|
-
chalk.cyan(' memoir share ') + chalk.gray('— share memory via secure link') + '\n' +
|
|
77
|
-
chalk.cyan(' memoir history ') + chalk.gray('— view backup versions') + '\n\n' +
|
|
78
|
-
chalk.gray(' Tip: use --profile work to sync a specific profile') + '\n\n' +
|
|
79
|
-
chalk.gray(`v${VERSION}`),
|
|
80
|
-
{ padding: 1, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
|
|
81
|
-
) + '\n');
|
|
82
|
-
process.exit(0);
|
|
58
|
+
// Pass 'push' as the command so Commander routes to pushCommand
|
|
59
|
+
process.argv.push('push');
|
|
83
60
|
}
|
|
84
61
|
|
|
85
62
|
// Custom help banner
|
|
86
63
|
program.addHelpText('beforeAll', '\n' + boxen(
|
|
87
64
|
gradient.pastel.multiline(' memoir ') + '\n' +
|
|
88
|
-
chalk.gray(' Your AI remembers everything.')
|
|
65
|
+
chalk.gray(' Your AI remembers everything.') + '\n\n' +
|
|
66
|
+
chalk.white.bold('Zero-config:') + ' just run ' + chalk.cyan('memoir') + ' or ' + chalk.cyan('npx memoir-cli') + '\n' +
|
|
67
|
+
chalk.gray('Auto-detects your GitHub, creates a private repo, and backs up.'),
|
|
89
68
|
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderStyle: 'round', borderColor: 'cyan', dimBorder: true }
|
|
90
69
|
) + '\n');
|
|
91
70
|
|
|
@@ -298,6 +277,30 @@ program
|
|
|
298
277
|
}
|
|
299
278
|
});
|
|
300
279
|
|
|
280
|
+
program
|
|
281
|
+
.command('activate')
|
|
282
|
+
.description('Add memoir instructions to this project so your AI uses it automatically')
|
|
283
|
+
.action(async () => {
|
|
284
|
+
try {
|
|
285
|
+
await activateCommand();
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.error(chalk.red('\n✖ Error:'), err.message);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
program
|
|
293
|
+
.command('deactivate')
|
|
294
|
+
.description('Remove memoir instructions from this project')
|
|
295
|
+
.action(async () => {
|
|
296
|
+
try {
|
|
297
|
+
await deactivateCommand();
|
|
298
|
+
} catch (err) {
|
|
299
|
+
console.error(chalk.red('\n✖ Error:'), err.message);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
301
304
|
program
|
|
302
305
|
.command('encrypt')
|
|
303
306
|
.description('Toggle E2E encryption for your backups')
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memoir-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"mcpName": "io.github.camgitt/memoir",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "MCP server that gives Claude, Cursor, and Gemini long-term memory across sessions. Your AI remembers your codebase, decisions, and preferences — across tools and machines.",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "node bin/memoir.js",
|
|
22
|
-
"test": "bash test-local.sh"
|
|
22
|
+
"test": "bash test-local.sh",
|
|
23
|
+
"postinstall": "node -e \"try{const c='\\x1b[36m',r='\\x1b[0m',g='\\x1b[90m';console.log('\\n '+c+'memoir'+r+' installed.\\n Run '+c+'memoir activate'+r+' in any project to give your AI long-term memory.\\n '+g+'https://memoir.sh'+r+'\\n')}catch{}\""
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
|
25
26
|
"mcp",
|
package/src/cloud/constants.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const SUPABASE_URL = process.env.MEMOIR_SUPABASE_URL || 'https://oqrkxytbahfwjhcbyzrx.supabase.co';
|
|
2
2
|
export const SUPABASE_ANON_KEY = process.env.MEMOIR_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xcmt4eXRiYWhmd2poY2J5enJ4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzMyMTQ4MzMsImV4cCI6MjA4ODc5MDgzM30.jOKOi73OJgIgi1zj0VOIQkGp0xqS3ee4gfCjpdqCnvM';
|
|
3
3
|
export const STORAGE_BUCKET = 'memoir-backups';
|
|
4
|
-
export const MAX_BACKUPS_FREE =
|
|
4
|
+
export const MAX_BACKUPS_FREE = 100;
|
|
5
5
|
export const MAX_BACKUPS_PRO = 50;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
|
|
7
|
+
// The instruction files each AI tool reads, in priority order
|
|
8
|
+
const INSTRUCTION_FILES = [
|
|
9
|
+
{ file: 'CLAUDE.md', tool: 'Claude' },
|
|
10
|
+
{ file: '.cursorrules', tool: 'Cursor' },
|
|
11
|
+
{ file: '.windsurfrules', tool: 'Windsurf' },
|
|
12
|
+
{ file: '.github/copilot-instructions.md', tool: 'Copilot' },
|
|
13
|
+
{ file: '.clinerules', tool: 'Cline' },
|
|
14
|
+
{ file: 'GEMINI.md', tool: 'Gemini' },
|
|
15
|
+
{ file: '.aider.system-prompt.md', tool: 'Aider' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const BLOCK_START = '<!-- memoir:start -->';
|
|
19
|
+
const BLOCK_END = '<!-- memoir:end -->';
|
|
20
|
+
|
|
21
|
+
const MEMOIR_BLOCK = `${BLOCK_START}
|
|
22
|
+
# Memoir — Persistent Memory
|
|
23
|
+
<!-- Cross-session memory for AI tools — https://memoir.sh -->
|
|
24
|
+
<!-- Install: npm i -g memoir-cli -->
|
|
25
|
+
Use memoir_recall to search past context before answering project questions.
|
|
26
|
+
Use memoir_remember to save important decisions, architecture choices, or context worth keeping.
|
|
27
|
+
${BLOCK_END}`;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Detect which instruction files exist in the current project.
|
|
31
|
+
* Returns array of { file, tool, fullPath, exists }
|
|
32
|
+
*/
|
|
33
|
+
function detectInstructionFiles(projectDir) {
|
|
34
|
+
return INSTRUCTION_FILES.map(({ file, tool }) => {
|
|
35
|
+
const fullPath = path.join(projectDir, file);
|
|
36
|
+
return { file, tool, fullPath, exists: fs.existsSync(fullPath) };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a file already has the memoir block
|
|
42
|
+
*/
|
|
43
|
+
function hasMemoir(content) {
|
|
44
|
+
return content.includes(BLOCK_START);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Inject memoir block into a file (append, or create)
|
|
49
|
+
*/
|
|
50
|
+
async function injectBlock(filePath) {
|
|
51
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
52
|
+
|
|
53
|
+
if (await fs.pathExists(filePath)) {
|
|
54
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
55
|
+
if (hasMemoir(content)) {
|
|
56
|
+
return 'already';
|
|
57
|
+
}
|
|
58
|
+
// Append with spacing
|
|
59
|
+
const separator = content.endsWith('\n') ? '\n' : '\n\n';
|
|
60
|
+
await fs.writeFile(filePath, content + separator + MEMOIR_BLOCK + '\n');
|
|
61
|
+
return 'appended';
|
|
62
|
+
} else {
|
|
63
|
+
await fs.writeFile(filePath, MEMOIR_BLOCK + '\n');
|
|
64
|
+
return 'created';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Remove memoir block from a file
|
|
70
|
+
*/
|
|
71
|
+
async function removeBlock(filePath) {
|
|
72
|
+
if (!await fs.pathExists(filePath)) return 'not_found';
|
|
73
|
+
|
|
74
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
75
|
+
if (!hasMemoir(content)) return 'not_present';
|
|
76
|
+
|
|
77
|
+
const startIdx = content.indexOf(BLOCK_START);
|
|
78
|
+
const endIdx = content.indexOf(BLOCK_END);
|
|
79
|
+
if (startIdx === -1 || endIdx === -1) return 'not_present';
|
|
80
|
+
|
|
81
|
+
const before = content.slice(0, startIdx).replace(/\n+$/, '');
|
|
82
|
+
const after = content.slice(endIdx + BLOCK_END.length).replace(/^\n+/, '');
|
|
83
|
+
const cleaned = (before + (before && after ? '\n\n' : '') + after).trim();
|
|
84
|
+
|
|
85
|
+
if (!cleaned) {
|
|
86
|
+
// File would be empty — delete it
|
|
87
|
+
await fs.remove(filePath);
|
|
88
|
+
return 'deleted';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await fs.writeFile(filePath, cleaned + '\n');
|
|
92
|
+
return 'removed';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Track which projects have been activated
|
|
97
|
+
*/
|
|
98
|
+
function getActivatedPath() {
|
|
99
|
+
return path.join(os.homedir(), '.config', 'memoir', 'activated-projects.json');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function loadActivated() {
|
|
103
|
+
const p = getActivatedPath();
|
|
104
|
+
if (await fs.pathExists(p)) {
|
|
105
|
+
const data = await fs.readJson(p);
|
|
106
|
+
return Array.isArray(data) ? data : [];
|
|
107
|
+
}
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function markActivated(projectDir) {
|
|
112
|
+
const activated = await loadActivated();
|
|
113
|
+
if (!activated.includes(projectDir)) {
|
|
114
|
+
activated.push(projectDir);
|
|
115
|
+
await fs.ensureDir(path.dirname(getActivatedPath()));
|
|
116
|
+
await fs.writeJson(getActivatedPath(), activated);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function markDeactivated(projectDir) {
|
|
121
|
+
let activated = await loadActivated();
|
|
122
|
+
activated = activated.filter(p => p !== projectDir);
|
|
123
|
+
await fs.ensureDir(path.dirname(getActivatedPath()));
|
|
124
|
+
await fs.writeJson(getActivatedPath(), activated);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function isActivated(projectDir) {
|
|
128
|
+
const activated = await loadActivated();
|
|
129
|
+
return activated.includes(projectDir);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* memoir activate — inject memoir instructions into project AI config files
|
|
134
|
+
*/
|
|
135
|
+
export async function activateCommand(options = {}) {
|
|
136
|
+
const projectDir = process.cwd();
|
|
137
|
+
const detected = detectInstructionFiles(projectDir);
|
|
138
|
+
const existing = detected.filter(d => d.exists);
|
|
139
|
+
|
|
140
|
+
if (existing.length === 0) {
|
|
141
|
+
// No instruction files exist — create CLAUDE.md by default
|
|
142
|
+
const result = await injectBlock(path.join(projectDir, 'CLAUDE.md'));
|
|
143
|
+
console.log(chalk.green('\n ✔ Created CLAUDE.md with memoir instructions'));
|
|
144
|
+
console.log(chalk.gray(' Your AI will use memoir_recall and memoir_remember automatically.\n'));
|
|
145
|
+
await markActivated(projectDir);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Inject into all existing instruction files
|
|
150
|
+
let injected = 0;
|
|
151
|
+
for (const { file, tool, fullPath } of existing) {
|
|
152
|
+
const result = await injectBlock(fullPath);
|
|
153
|
+
if (result === 'appended') {
|
|
154
|
+
console.log(chalk.green(` ✔ Added memoir to ${file}`) + chalk.gray(` (${tool})`));
|
|
155
|
+
injected++;
|
|
156
|
+
} else if (result === 'created') {
|
|
157
|
+
console.log(chalk.green(` ✔ Created ${file} with memoir instructions`) + chalk.gray(` (${tool})`));
|
|
158
|
+
injected++;
|
|
159
|
+
} else if (result === 'already') {
|
|
160
|
+
console.log(chalk.gray(` · ${file} already has memoir`) + chalk.gray(` (${tool})`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (injected > 0) {
|
|
165
|
+
console.log(chalk.gray('\n Your AI tools will now use memoir automatically.\n'));
|
|
166
|
+
} else {
|
|
167
|
+
console.log(chalk.gray('\n memoir is already active in this project.\n'));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await markActivated(projectDir);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* memoir deactivate — remove memoir instructions from project AI config files
|
|
175
|
+
*/
|
|
176
|
+
export async function deactivateCommand(options = {}) {
|
|
177
|
+
const projectDir = process.cwd();
|
|
178
|
+
const detected = detectInstructionFiles(projectDir);
|
|
179
|
+
|
|
180
|
+
let removed = 0;
|
|
181
|
+
for (const { file, tool, fullPath } of detected) {
|
|
182
|
+
const result = await removeBlock(fullPath);
|
|
183
|
+
if (result === 'removed') {
|
|
184
|
+
console.log(chalk.yellow(` ✔ Removed memoir from ${file}`) + chalk.gray(` (${tool})`));
|
|
185
|
+
removed++;
|
|
186
|
+
} else if (result === 'deleted') {
|
|
187
|
+
console.log(chalk.yellow(` ✔ Deleted ${file}`) + chalk.gray(' (was only memoir block)'));
|
|
188
|
+
removed++;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (removed === 0) {
|
|
193
|
+
console.log(chalk.gray(' · memoir is not active in this project.\n'));
|
|
194
|
+
} else {
|
|
195
|
+
console.log(chalk.gray('\n memoir instructions removed.\n'));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await markDeactivated(projectDir);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Prompt to activate — called from push on first push per project
|
|
203
|
+
*/
|
|
204
|
+
export async function promptActivate() {
|
|
205
|
+
const projectDir = process.cwd();
|
|
206
|
+
|
|
207
|
+
// Don't prompt if already activated or if not in a project directory
|
|
208
|
+
if (await isActivated(projectDir)) return;
|
|
209
|
+
|
|
210
|
+
// Check if we're in a project (has git, or has instruction files, or has package.json etc.)
|
|
211
|
+
const projectSignals = ['.git', 'package.json', 'Cargo.toml', 'go.mod', 'pyproject.toml', 'Makefile'];
|
|
212
|
+
const isProject = projectSignals.some(f => fs.existsSync(path.join(projectDir, f)));
|
|
213
|
+
if (!isProject) {
|
|
214
|
+
await markActivated(projectDir); // Don't ask again for non-projects
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.log('');
|
|
219
|
+
const { activate } = await inquirer.prompt([{
|
|
220
|
+
type: 'confirm',
|
|
221
|
+
name: 'activate',
|
|
222
|
+
message: 'Add memoir instructions to this project so your AI uses it automatically?',
|
|
223
|
+
default: true,
|
|
224
|
+
}]);
|
|
225
|
+
|
|
226
|
+
if (activate) {
|
|
227
|
+
await activateCommand();
|
|
228
|
+
} else {
|
|
229
|
+
await markActivated(projectDir); // Don't ask again
|
|
230
|
+
console.log(chalk.gray(' · Skipped. Run ') + chalk.cyan('memoir activate') + chalk.gray(' anytime to enable.\n'));
|
|
231
|
+
}
|
|
232
|
+
}
|
package/src/commands/push.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'os';
|
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
import boxen from 'boxen';
|
|
7
7
|
import gradient from 'gradient-string';
|
|
8
|
-
import { getConfig } from '../config.js';
|
|
8
|
+
import { getConfig, autoSetup } from '../config.js';
|
|
9
9
|
import { extractMemories, adapters } from '../adapters/index.js';
|
|
10
10
|
import { syncToLocal, syncToGit } from '../providers/index.js';
|
|
11
11
|
import inquirer from 'inquirer';
|
|
@@ -14,17 +14,25 @@ import { scanForSecrets, printSecurityReport } from '../security/scanner.js';
|
|
|
14
14
|
import { encryptDirectory, createVerifyToken } from '../security/encryption.js';
|
|
15
15
|
import { getRawConfig, saveConfig, migrateConfigToV2 } from '../config.js';
|
|
16
16
|
import { scanWorkspace } from '../workspace/tracker.js';
|
|
17
|
+
import { promptActivate } from './activate.js';
|
|
17
18
|
|
|
18
19
|
export async function pushCommand(options = {}) {
|
|
19
|
-
|
|
20
|
+
let config = await getConfig(options.profile);
|
|
20
21
|
|
|
21
22
|
if (!config) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
// Zero-config: auto-detect GitHub user, create repo, save config
|
|
24
|
+
const setupSpinner = ora({ text: chalk.gray('Setting up memoir automatically...'), spinner: 'dots' }).start();
|
|
25
|
+
config = await autoSetup();
|
|
26
|
+
if (config) {
|
|
27
|
+
setupSpinner.succeed(chalk.green('Auto-configured') + chalk.gray(` → ${config.gitRepo}`));
|
|
28
|
+
} else {
|
|
29
|
+
setupSpinner.fail(chalk.red('Could not detect GitHub username'));
|
|
30
|
+
console.log('\n' + boxen(
|
|
31
|
+
chalk.white('Run ') + chalk.cyan.bold('memoir init') + chalk.white(' to set up manually.'),
|
|
32
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'yellow' }
|
|
33
|
+
) + '\n');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
console.log();
|
|
@@ -262,6 +270,13 @@ export async function pushCommand(options = {}) {
|
|
|
262
270
|
chalk.gray('Restore on another machine with: ') + chalk.cyan('memoir restore'),
|
|
263
271
|
{ padding: 1, borderStyle: 'round', borderColor: 'green', dimBorder: true }
|
|
264
272
|
) + '\n');
|
|
273
|
+
|
|
274
|
+
// Prompt to activate memoir in this project (first push only)
|
|
275
|
+
try {
|
|
276
|
+
await promptActivate();
|
|
277
|
+
} catch {
|
|
278
|
+
// Activation prompt is best-effort
|
|
279
|
+
}
|
|
265
280
|
} catch (error) {
|
|
266
281
|
spinner.fail(chalk.red('Sync failed: ') + error.message);
|
|
267
282
|
} finally {
|
package/src/commands/restore.js
CHANGED
|
@@ -6,7 +6,7 @@ import ora from 'ora';
|
|
|
6
6
|
import boxen from 'boxen';
|
|
7
7
|
import gradient from 'gradient-string';
|
|
8
8
|
import inquirer from 'inquirer';
|
|
9
|
-
import { getConfig } from '../config.js';
|
|
9
|
+
import { getConfig, autoSetup } from '../config.js';
|
|
10
10
|
import { fetchFromLocal, fetchFromGit } from '../providers/restore.js';
|
|
11
11
|
import { decryptDirectory, verifyPassphrase } from '../security/encryption.js';
|
|
12
12
|
import { detectLocalHomeKey } from '../adapters/restore.js';
|
|
@@ -23,15 +23,21 @@ export async function restoreCommand(options = {}) {
|
|
|
23
23
|
return restoreFromShare(options);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
let config = await getConfig(options.profile);
|
|
27
27
|
|
|
28
28
|
if (!config) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
const setupSpinner = ora({ text: chalk.gray('Setting up memoir automatically...'), spinner: 'dots' }).start();
|
|
30
|
+
config = await autoSetup();
|
|
31
|
+
if (config) {
|
|
32
|
+
setupSpinner.succeed(chalk.green('Auto-configured') + chalk.gray(` → ${config.gitRepo}`));
|
|
33
|
+
} else {
|
|
34
|
+
setupSpinner.fail(chalk.red('Could not detect GitHub username'));
|
|
35
|
+
console.log('\n' + boxen(
|
|
36
|
+
chalk.white('Run ') + chalk.cyan.bold('memoir init') + chalk.white(' to set up manually.'),
|
|
37
|
+
{ padding: 1, borderStyle: 'round', borderColor: 'yellow' }
|
|
38
|
+
) + '\n');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
console.log();
|
package/src/commands/upgrade.js
CHANGED
|
@@ -63,7 +63,7 @@ export async function upgradeCommand() {
|
|
|
63
63
|
const sep = chalk.gray('─'.repeat(col1 + col2 + 18));
|
|
64
64
|
|
|
65
65
|
const rows = [
|
|
66
|
-
[chalk.gray('
|
|
66
|
+
[chalk.gray('100 cloud backups'), chalk.white('Unlimited backups'), chalk.white('Unlimited backups')],
|
|
67
67
|
[chalk.gray('Local only'), chalk.white('Unlimited machines'), chalk.white('Shared team context')],
|
|
68
68
|
[chalk.gray('Manual snapshots'), chalk.white('Auto snapshots'), chalk.white('Team dashboard')],
|
|
69
69
|
[chalk.gray('Community support'), chalk.white('Priority support'), chalk.white('Audit log')],
|
package/src/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
+
import { execFileSync } from 'child_process';
|
|
4
5
|
|
|
5
6
|
const CONFIG_DIR = process.platform === 'win32'
|
|
6
7
|
? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'memoir')
|
|
@@ -114,6 +115,50 @@ export async function deleteProfile(name) {
|
|
|
114
115
|
await saveConfig(raw);
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
// Zero-config auto-setup: detect GitHub user, create repo, save config, return it
|
|
119
|
+
export async function autoSetup() {
|
|
120
|
+
// Try gh CLI first, then git config
|
|
121
|
+
let username = '';
|
|
122
|
+
try {
|
|
123
|
+
username = execFileSync('gh', ['api', 'user', '--jq', '.login'], { encoding: 'utf8', timeout: 5000 }).trim();
|
|
124
|
+
} catch {
|
|
125
|
+
try {
|
|
126
|
+
username = execFileSync('git', ['config', '--global', 'user.name'], { encoding: 'utf8' }).trim();
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!username) return null; // Can't auto-setup without a username
|
|
131
|
+
|
|
132
|
+
const repo = 'ai-memory';
|
|
133
|
+
const gitRepo = `https://github.com/${username}/${repo}.git`;
|
|
134
|
+
|
|
135
|
+
// Try to create the repo if it doesn't exist (best-effort)
|
|
136
|
+
try {
|
|
137
|
+
execFileSync('gh', ['repo', 'view', `${username}/${repo}`], { stdio: 'ignore', timeout: 5000 });
|
|
138
|
+
} catch {
|
|
139
|
+
try {
|
|
140
|
+
execFileSync('gh', ['repo', 'create', `${username}/${repo}`, '--private', '--description', 'AI memory backup (memoir-cli)'], { stdio: 'ignore', timeout: 10000 });
|
|
141
|
+
} catch {
|
|
142
|
+
// If gh isn't available, user will need to create repo manually — that's fine, syncToGit will handle it
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const config = {
|
|
147
|
+
version: 2,
|
|
148
|
+
activeProfile: 'default',
|
|
149
|
+
profiles: {
|
|
150
|
+
default: {
|
|
151
|
+
provider: 'git',
|
|
152
|
+
gitRepo,
|
|
153
|
+
encrypt: false // Skip encryption for zero-config — user can enable later with `memoir encrypt`
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
await saveConfig(config);
|
|
159
|
+
return config.profiles.default;
|
|
160
|
+
}
|
|
161
|
+
|
|
117
162
|
export async function getGeminiApiKey() {
|
|
118
163
|
const raw = await getRawConfig();
|
|
119
164
|
return raw?.geminiApiKey || process.env.GEMINI_API_KEY || null;
|