@squidcode/forever-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # @squidcode/forever-plugin
2
+
3
+ MCP (Model Context Protocol) plugin for [Forever](https://forever.squidcode.com) — a centralized persistent memory layer for Claude Code instances.
4
+
5
+ Forever lets multiple Claude Code sessions share memory across machines, projects, and time. This plugin connects Claude Code to your Forever server via MCP.
6
+
7
+ ## Prerequisites
8
+
9
+ - [Node.js](https://nodejs.org/) 18+
10
+ - A Forever account ([register here](https://forever.squidcode.com/register))
11
+
12
+ ## Setup
13
+
14
+ ### 1. Authenticate
15
+
16
+ ```bash
17
+ npx @squidcode/forever-plugin login
18
+ ```
19
+
20
+ You'll be prompted for:
21
+ - **Server URL**: `https://forever.squidcode.com`
22
+ - **Email**: your registered email
23
+ - **Password**: your password
24
+
25
+ Credentials are saved to `~/.forever/credentials.json` (mode 0600).
26
+
27
+ ### 2. Add to Claude Code
28
+
29
+ ```bash
30
+ claude mcp add forever -- npx @squidcode/forever-plugin
31
+ ```
32
+
33
+ This registers the plugin as an MCP server that Claude Code will start automatically.
34
+
35
+ ## Tools
36
+
37
+ The plugin exposes three MCP tools:
38
+
39
+ ### `memory_log`
40
+
41
+ Log an entry to Forever memory.
42
+
43
+ | Parameter | Type | Required | Description |
44
+ |-------------|----------|----------|--------------------------------------|
45
+ | `project` | string | yes | Project name or git remote URL |
46
+ | `type` | enum | yes | `summary`, `decision`, or `error` |
47
+ | `content` | string | yes | The content to log |
48
+ | `tags` | string[] | no | Tags for categorization |
49
+ | `sessionId` | string | no | Session ID for grouping entries |
50
+
51
+ ### `memory_get_recent`
52
+
53
+ Get recent memory entries for a project.
54
+
55
+ | Parameter | Type | Required | Description |
56
+ |-----------|--------|----------|--------------------------------|
57
+ | `project` | string | yes | Project name or git remote URL |
58
+ | `limit` | number | no | Number of entries (default 20) |
59
+
60
+ ### `memory_search`
61
+
62
+ Search memory entries across projects.
63
+
64
+ | Parameter | Type | Required | Description |
65
+ |-----------|--------|----------|------------------------|
66
+ | `query` | string | yes | Search query |
67
+ | `project` | string | no | Filter by project |
68
+ | `type` | enum | no | Filter by entry type |
69
+ | `limit` | number | no | Max results (default 20) |
70
+
71
+ ## How It Works
72
+
73
+ - The plugin runs as an MCP stdio server, started by Claude Code on demand.
74
+ - Each machine gets a unique ID (stored in `~/.forever/machine.json`) for tracking which machine produced each memory entry.
75
+ - All API calls are authenticated via JWT token obtained during login.
76
+
77
+ ## Development
78
+
79
+ ```bash
80
+ git clone https://github.com/squidcode/forever-plugin.git
81
+ cd forever-plugin
82
+ npm install
83
+ npm run build
84
+ ```
85
+
86
+ ### Scripts
87
+
88
+ | Script | Description |
89
+ |-----------------|------------------------------|
90
+ | `npm run build` | Compile TypeScript |
91
+ | `npm run dev` | Watch mode |
92
+ | `npm run lint` | Run ESLint |
93
+ | `npm run format`| Format with Prettier |
94
+ | `npm run typecheck` | Type-check without emit |
95
+
96
+ ### Pre-commit Hooks
97
+
98
+ This project uses [Husky](https://typicode.github.io/husky/) + [lint-staged](https://github.com/lint-staged/lint-staged) to enforce code quality on every commit:
99
+
100
+ - ESLint auto-fix + Prettier formatting on staged `.ts` files
101
+ - Full TypeScript type-check
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1,15 @@
1
+ import { type AxiosInstance } from 'axios';
2
+ interface Credentials {
3
+ serverUrl: string;
4
+ token: string;
5
+ }
6
+ interface MachineConfig {
7
+ machineId: string;
8
+ alias: string;
9
+ }
10
+ export declare function getCredentials(): Credentials | null;
11
+ export declare function saveCredentials(creds: Credentials): void;
12
+ export declare function getMachineConfig(): MachineConfig | null;
13
+ export declare function saveMachineConfig(config: MachineConfig): void;
14
+ export declare function createApiClient(): AxiosInstance | null;
15
+ export {};
package/dist/client.js ADDED
@@ -0,0 +1,57 @@
1
+ import axios from 'axios';
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+ const CONFIG_DIR = join(homedir(), '.forever');
6
+ const CREDENTIALS_FILE = join(CONFIG_DIR, 'credentials.json');
7
+ const MACHINE_FILE = join(CONFIG_DIR, 'machine.json');
8
+ function ensureConfigDir() {
9
+ if (!existsSync(CONFIG_DIR)) {
10
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
11
+ }
12
+ }
13
+ export function getCredentials() {
14
+ if (!existsSync(CREDENTIALS_FILE))
15
+ return null;
16
+ try {
17
+ return JSON.parse(readFileSync(CREDENTIALS_FILE, 'utf-8'));
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export function saveCredentials(creds) {
24
+ ensureConfigDir();
25
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), {
26
+ mode: 0o600,
27
+ });
28
+ }
29
+ export function getMachineConfig() {
30
+ if (!existsSync(MACHINE_FILE))
31
+ return null;
32
+ try {
33
+ return JSON.parse(readFileSync(MACHINE_FILE, 'utf-8'));
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export function saveMachineConfig(config) {
40
+ ensureConfigDir();
41
+ writeFileSync(MACHINE_FILE, JSON.stringify(config, null, 2), {
42
+ mode: 0o600,
43
+ });
44
+ }
45
+ export function createApiClient() {
46
+ const creds = getCredentials();
47
+ if (!creds)
48
+ return null;
49
+ return axios.create({
50
+ baseURL: creds.serverUrl.replace(/\/$/, '') + '/api',
51
+ headers: {
52
+ Authorization: `Bearer ${creds.token}`,
53
+ 'Content-Type': 'application/json',
54
+ },
55
+ timeout: 10000,
56
+ });
57
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { createApiClient } from './client.js';
6
+ import { getOrCreateMachineId } from './machine.js';
7
+ const server = new McpServer({
8
+ name: 'forever',
9
+ version: '0.1.0',
10
+ });
11
+ const machineId = getOrCreateMachineId();
12
+ server.tool('memory_log', 'Log an entry to Forever memory (summary, decision, or error)', {
13
+ project: z.string().describe('Project name or git remote URL'),
14
+ type: z
15
+ .enum(['summary', 'decision', 'error'])
16
+ .describe('Type of memory entry'),
17
+ content: z.string().describe('The content to log'),
18
+ tags: z
19
+ .array(z.string())
20
+ .optional()
21
+ .describe('Optional tags for categorization'),
22
+ sessionId: z.string().optional().describe('Session ID for grouping'),
23
+ }, async ({ project, type, content, tags, sessionId }) => {
24
+ const api = createApiClient();
25
+ if (!api) {
26
+ return {
27
+ content: [
28
+ {
29
+ type: 'text',
30
+ text: 'Not authenticated. Run: forever-plugin login',
31
+ },
32
+ ],
33
+ };
34
+ }
35
+ try {
36
+ await api.post('/logs', {
37
+ project,
38
+ type,
39
+ content,
40
+ machineId,
41
+ tags,
42
+ sessionId,
43
+ });
44
+ return {
45
+ content: [{ type: 'text', text: `Logged ${type} entry.` }],
46
+ };
47
+ }
48
+ catch (err) {
49
+ return {
50
+ content: [
51
+ {
52
+ type: 'text',
53
+ text: `Failed to log: ${err.message}`,
54
+ },
55
+ ],
56
+ };
57
+ }
58
+ });
59
+ server.tool('memory_get_recent', 'Get recent memory entries for a project', {
60
+ project: z.string().describe('Project name or git remote URL'),
61
+ limit: z
62
+ .number()
63
+ .optional()
64
+ .default(20)
65
+ .describe('Number of entries to fetch'),
66
+ }, async ({ project, limit }) => {
67
+ const api = createApiClient();
68
+ if (!api) {
69
+ return {
70
+ content: [
71
+ {
72
+ type: 'text',
73
+ text: 'Not authenticated. Run: forever-plugin login',
74
+ },
75
+ ],
76
+ };
77
+ }
78
+ try {
79
+ const res = await api.get('/logs/recent', {
80
+ params: { project, limit },
81
+ });
82
+ const logs = res.data;
83
+ if (!logs.length) {
84
+ return {
85
+ content: [
86
+ {
87
+ type: 'text',
88
+ text: `No memory entries found for project "${project}".`,
89
+ },
90
+ ],
91
+ };
92
+ }
93
+ const formatted = logs
94
+ .map((log) => `[${log.type}] ${log.createdAt}\n${log.content}${log.tags?.length ? `\ntags: ${log.tags.join(', ')}` : ''}`)
95
+ .join('\n---\n');
96
+ return { content: [{ type: 'text', text: formatted }] };
97
+ }
98
+ catch (err) {
99
+ return {
100
+ content: [
101
+ {
102
+ type: 'text',
103
+ text: `Failed to fetch: ${err.message}`,
104
+ },
105
+ ],
106
+ };
107
+ }
108
+ });
109
+ server.tool('memory_search', 'Search memory entries across projects', {
110
+ query: z.string().describe('Search query'),
111
+ project: z.string().optional().describe('Filter by project'),
112
+ type: z
113
+ .enum(['user_input', 'claude_reply', 'summary', 'decision', 'error'])
114
+ .optional()
115
+ .describe('Filter by entry type'),
116
+ limit: z.number().optional().default(20).describe('Max results'),
117
+ }, async ({ query, project, type, limit }) => {
118
+ const api = createApiClient();
119
+ if (!api) {
120
+ return {
121
+ content: [
122
+ {
123
+ type: 'text',
124
+ text: 'Not authenticated. Run: forever-plugin login',
125
+ },
126
+ ],
127
+ };
128
+ }
129
+ try {
130
+ const params = { query, limit };
131
+ if (project)
132
+ params.project = project;
133
+ if (type)
134
+ params.type = type;
135
+ const res = await api.get('/logs/search', { params });
136
+ const logs = res.data;
137
+ if (!logs.length) {
138
+ return {
139
+ content: [
140
+ {
141
+ type: 'text',
142
+ text: `No results for "${query}".`,
143
+ },
144
+ ],
145
+ };
146
+ }
147
+ const formatted = logs
148
+ .map((log) => `[${log.type}] ${log.project} - ${log.createdAt}\n${log.content}`)
149
+ .join('\n---\n');
150
+ return { content: [{ type: 'text', text: formatted }] };
151
+ }
152
+ catch (err) {
153
+ return {
154
+ content: [
155
+ {
156
+ type: 'text',
157
+ text: `Search failed: ${err.message}`,
158
+ },
159
+ ],
160
+ };
161
+ }
162
+ });
163
+ // Login subcommand
164
+ if (process.argv[2] === 'login') {
165
+ const readline = await import('readline');
166
+ const rl = readline.createInterface({
167
+ input: process.stdin,
168
+ output: process.stdout,
169
+ });
170
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
171
+ console.log('Forever Plugin Login\n');
172
+ const serverUrl = await ask('Server URL (e.g. https://forever-z44hy.ondigitalocean.app): ');
173
+ const email = await ask('Email: ');
174
+ const password = await ask('Password: ');
175
+ try {
176
+ const { default: axios } = await import('axios');
177
+ const res = await axios.post(`${serverUrl.replace(/\/$/, '')}/api/auth/login`, {
178
+ email,
179
+ password,
180
+ });
181
+ const { saveCredentials } = await import('./client.js');
182
+ saveCredentials({ serverUrl, token: res.data.access_token });
183
+ console.log('\nAuthenticated! Credentials saved to ~/.forever/credentials.json');
184
+ }
185
+ catch (err) {
186
+ console.error('\nLogin failed:', err.response?.data?.message || err.message);
187
+ process.exit(1);
188
+ }
189
+ rl.close();
190
+ process.exit(0);
191
+ }
192
+ // Start MCP server
193
+ const transport = new StdioServerTransport();
194
+ await server.connect(transport);
@@ -0,0 +1 @@
1
+ export declare function getOrCreateMachineId(): string;
@@ -0,0 +1,11 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { hostname } from 'os';
3
+ import { getMachineConfig, saveMachineConfig } from './client.js';
4
+ export function getOrCreateMachineId() {
5
+ const config = getMachineConfig();
6
+ if (config?.machineId)
7
+ return config.machineId;
8
+ const id = `${hostname()}-${randomBytes(4).toString('hex')}`;
9
+ saveMachineConfig({ machineId: id, alias: hostname() });
10
+ return id;
11
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@squidcode/forever-plugin",
3
+ "version": "0.1.0",
4
+ "description": "MCP plugin for Forever - Claude Memory System",
5
+ "type": "module",
6
+ "bin": {
7
+ "forever-plugin": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsc --watch",
15
+ "start": "node dist/index.js",
16
+ "lint": "eslint 'src/**/*.ts'",
17
+ "format": "prettier --write 'src/**/*.ts'",
18
+ "format:check": "prettier --check 'src/**/*.ts'",
19
+ "typecheck": "tsc --noEmit",
20
+ "prepare": "husky",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "lint-staged": {
24
+ "*.ts": [
25
+ "eslint --fix",
26
+ "prettier --write"
27
+ ]
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.0.0",
31
+ "axios": "^1.7.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.0.0",
35
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
36
+ "@typescript-eslint/parser": "^8.56.0",
37
+ "eslint": "^10.0.1",
38
+ "eslint-config-prettier": "^10.1.8",
39
+ "eslint-plugin-prettier": "^5.5.5",
40
+ "husky": "^9.1.7",
41
+ "lint-staged": "^16.2.7",
42
+ "prettier": "^3.8.1",
43
+ "typescript": "^5.7.0"
44
+ },
45
+ "keywords": [
46
+ "mcp",
47
+ "claude",
48
+ "memory",
49
+ "forever"
50
+ ],
51
+ "license": "MIT",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/squidcode/forever-plugin.git"
55
+ }
56
+ }