@squidcode/forever-plugin 0.1.0 → 0.3.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/dist/index.js +166 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,15 +2,51 @@
|
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { execFileSync } from 'child_process';
|
|
6
|
+
import { randomBytes } from 'crypto';
|
|
7
|
+
import { basename } from 'path';
|
|
5
8
|
import { createApiClient } from './client.js';
|
|
6
9
|
import { getOrCreateMachineId } from './machine.js';
|
|
7
10
|
const server = new McpServer({
|
|
8
11
|
name: 'forever',
|
|
9
|
-
version: '0.
|
|
12
|
+
version: '0.3.0',
|
|
10
13
|
});
|
|
11
14
|
const machineId = getOrCreateMachineId();
|
|
15
|
+
const sessionId = `${Date.now()}-${randomBytes(4).toString('hex')}`;
|
|
16
|
+
function git(...args) {
|
|
17
|
+
try {
|
|
18
|
+
return execFileSync('git', args, {
|
|
19
|
+
encoding: 'utf-8',
|
|
20
|
+
timeout: 5000,
|
|
21
|
+
}).trim();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function getGitContext() {
|
|
28
|
+
return {
|
|
29
|
+
gitBranch: git('rev-parse', '--abbrev-ref', 'HEAD'),
|
|
30
|
+
gitCommit: git('rev-parse', '--short', 'HEAD'),
|
|
31
|
+
directory: process.cwd(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function resolveProject(explicit) {
|
|
35
|
+
if (explicit)
|
|
36
|
+
return explicit;
|
|
37
|
+
const remote = git('remote', 'get-url', 'origin');
|
|
38
|
+
if (remote)
|
|
39
|
+
return remote;
|
|
40
|
+
const dir = process.cwd();
|
|
41
|
+
if (dir && dir !== '/')
|
|
42
|
+
return basename(dir);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
12
45
|
server.tool('memory_log', 'Log an entry to Forever memory (summary, decision, or error)', {
|
|
13
|
-
project: z
|
|
46
|
+
project: z
|
|
47
|
+
.string()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('Project name or git remote URL (auto-detected from git if omitted)'),
|
|
14
50
|
type: z
|
|
15
51
|
.enum(['summary', 'decision', 'error'])
|
|
16
52
|
.describe('Type of memory entry'),
|
|
@@ -19,30 +55,51 @@ server.tool('memory_log', 'Log an entry to Forever memory (summary, decision, or
|
|
|
19
55
|
.array(z.string())
|
|
20
56
|
.optional()
|
|
21
57
|
.describe('Optional tags for categorization'),
|
|
22
|
-
sessionId: z
|
|
23
|
-
|
|
58
|
+
sessionId: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe('Session ID for grouping (auto-generated if omitted)'),
|
|
62
|
+
}, async ({ project, type, content, tags, sessionId: explicitSessionId }) => {
|
|
24
63
|
const api = createApiClient();
|
|
25
64
|
if (!api) {
|
|
26
65
|
return {
|
|
27
66
|
content: [
|
|
28
67
|
{
|
|
29
68
|
type: 'text',
|
|
30
|
-
text: 'Not authenticated. Run: forever-plugin login',
|
|
69
|
+
text: 'Not authenticated. Run: npx @squidcode/forever-plugin login',
|
|
31
70
|
},
|
|
32
71
|
],
|
|
33
72
|
};
|
|
34
73
|
}
|
|
74
|
+
const resolvedProject = resolveProject(project);
|
|
75
|
+
if (!resolvedProject) {
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: 'text',
|
|
80
|
+
text: 'Could not detect project. Please specify a project name.',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const gitContext = getGitContext();
|
|
35
86
|
try {
|
|
36
87
|
await api.post('/logs', {
|
|
37
|
-
project,
|
|
88
|
+
project: resolvedProject,
|
|
38
89
|
type,
|
|
39
90
|
content,
|
|
40
91
|
machineId,
|
|
41
92
|
tags,
|
|
42
|
-
sessionId,
|
|
93
|
+
sessionId: explicitSessionId || sessionId,
|
|
94
|
+
...gitContext,
|
|
43
95
|
});
|
|
44
96
|
return {
|
|
45
|
-
content: [
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: `Logged ${type} entry for "${resolvedProject}".`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
46
103
|
};
|
|
47
104
|
}
|
|
48
105
|
catch (err) {
|
|
@@ -57,7 +114,10 @@ server.tool('memory_log', 'Log an entry to Forever memory (summary, decision, or
|
|
|
57
114
|
}
|
|
58
115
|
});
|
|
59
116
|
server.tool('memory_get_recent', 'Get recent memory entries for a project', {
|
|
60
|
-
project: z
|
|
117
|
+
project: z
|
|
118
|
+
.string()
|
|
119
|
+
.optional()
|
|
120
|
+
.describe('Project name or git remote URL (auto-detected from git if omitted)'),
|
|
61
121
|
limit: z
|
|
62
122
|
.number()
|
|
63
123
|
.optional()
|
|
@@ -70,14 +130,25 @@ server.tool('memory_get_recent', 'Get recent memory entries for a project', {
|
|
|
70
130
|
content: [
|
|
71
131
|
{
|
|
72
132
|
type: 'text',
|
|
73
|
-
text: 'Not authenticated. Run: forever-plugin login',
|
|
133
|
+
text: 'Not authenticated. Run: npx @squidcode/forever-plugin login',
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const resolvedProject = resolveProject(project);
|
|
139
|
+
if (!resolvedProject) {
|
|
140
|
+
return {
|
|
141
|
+
content: [
|
|
142
|
+
{
|
|
143
|
+
type: 'text',
|
|
144
|
+
text: 'Could not detect project. Please specify a project name.',
|
|
74
145
|
},
|
|
75
146
|
],
|
|
76
147
|
};
|
|
77
148
|
}
|
|
78
149
|
try {
|
|
79
150
|
const res = await api.get('/logs/recent', {
|
|
80
|
-
params: { project, limit },
|
|
151
|
+
params: { project: resolvedProject, limit },
|
|
81
152
|
});
|
|
82
153
|
const logs = res.data;
|
|
83
154
|
if (!logs.length) {
|
|
@@ -85,7 +156,7 @@ server.tool('memory_get_recent', 'Get recent memory entries for a project', {
|
|
|
85
156
|
content: [
|
|
86
157
|
{
|
|
87
158
|
type: 'text',
|
|
88
|
-
text: `No memory entries found for project "${
|
|
159
|
+
text: `No memory entries found for project "${resolvedProject}".`,
|
|
89
160
|
},
|
|
90
161
|
],
|
|
91
162
|
};
|
|
@@ -106,6 +177,85 @@ server.tool('memory_get_recent', 'Get recent memory entries for a project', {
|
|
|
106
177
|
};
|
|
107
178
|
}
|
|
108
179
|
});
|
|
180
|
+
server.tool('memory_get_sessions', 'Get recent sessions for a project, grouped by session with machine info. Use at startup to detect cross-machine handoffs.', {
|
|
181
|
+
project: z
|
|
182
|
+
.string()
|
|
183
|
+
.optional()
|
|
184
|
+
.describe('Project name or git remote URL (auto-detected from git if omitted)'),
|
|
185
|
+
limit: z
|
|
186
|
+
.number()
|
|
187
|
+
.optional()
|
|
188
|
+
.default(10)
|
|
189
|
+
.describe('Number of recent sessions to fetch'),
|
|
190
|
+
}, async ({ project, limit }) => {
|
|
191
|
+
const api = createApiClient();
|
|
192
|
+
if (!api) {
|
|
193
|
+
return {
|
|
194
|
+
content: [
|
|
195
|
+
{
|
|
196
|
+
type: 'text',
|
|
197
|
+
text: 'Not authenticated. Run: npx @squidcode/forever-plugin login',
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const resolvedProject = resolveProject(project);
|
|
203
|
+
if (!resolvedProject) {
|
|
204
|
+
return {
|
|
205
|
+
content: [
|
|
206
|
+
{
|
|
207
|
+
type: 'text',
|
|
208
|
+
text: 'Could not detect project. Please specify a project name.',
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const res = await api.get('/logs/sessions', {
|
|
215
|
+
params: { project: resolvedProject, machineId, limit },
|
|
216
|
+
});
|
|
217
|
+
const { sessions, hasRemoteActivity } = res.data;
|
|
218
|
+
if (!sessions.length) {
|
|
219
|
+
return {
|
|
220
|
+
content: [
|
|
221
|
+
{
|
|
222
|
+
type: 'text',
|
|
223
|
+
text: `No previous sessions found for "${resolvedProject}".`,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const lines = [];
|
|
229
|
+
if (hasRemoteActivity) {
|
|
230
|
+
lines.push('⚡ REMOTE ACTIVITY DETECTED — Sessions from other machines found for this project.\n');
|
|
231
|
+
}
|
|
232
|
+
for (const s of sessions) {
|
|
233
|
+
const machine = s.machineName || 'unknown';
|
|
234
|
+
const remote = s.isRemote ? ' [REMOTE]' : ' [LOCAL]';
|
|
235
|
+
const branch = s.gitBranch ? ` on ${s.gitBranch}` : '';
|
|
236
|
+
const commit = s.gitCommit ? ` @ ${s.gitCommit}` : '';
|
|
237
|
+
lines.push(`## Session ${s.sessionId}${remote}`);
|
|
238
|
+
lines.push(`Machine: ${machine}${branch}${commit}`);
|
|
239
|
+
lines.push(`Time: ${s.startedAt} → ${s.endedAt} (${s.logCount} logs)`);
|
|
240
|
+
if (s.directory)
|
|
241
|
+
lines.push(`Directory: ${s.directory}`);
|
|
242
|
+
if (s.summary)
|
|
243
|
+
lines.push(`Summary: ${s.summary}`);
|
|
244
|
+
lines.push('');
|
|
245
|
+
}
|
|
246
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
return {
|
|
250
|
+
content: [
|
|
251
|
+
{
|
|
252
|
+
type: 'text',
|
|
253
|
+
text: `Failed to fetch sessions: ${err.message}`,
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
});
|
|
109
259
|
server.tool('memory_search', 'Search memory entries across projects', {
|
|
110
260
|
query: z.string().describe('Search query'),
|
|
111
261
|
project: z.string().optional().describe('Filter by project'),
|
|
@@ -121,7 +271,7 @@ server.tool('memory_search', 'Search memory entries across projects', {
|
|
|
121
271
|
content: [
|
|
122
272
|
{
|
|
123
273
|
type: 'text',
|
|
124
|
-
text: 'Not authenticated. Run: forever-plugin login',
|
|
274
|
+
text: 'Not authenticated. Run: npx @squidcode/forever-plugin login',
|
|
125
275
|
},
|
|
126
276
|
],
|
|
127
277
|
};
|
|
@@ -169,7 +319,9 @@ if (process.argv[2] === 'login') {
|
|
|
169
319
|
});
|
|
170
320
|
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
171
321
|
console.log('Forever Plugin Login\n');
|
|
172
|
-
const
|
|
322
|
+
const DEFAULT_SERVER = 'https://forever.squidcode.com';
|
|
323
|
+
const serverUrlInput = await ask(`Server URL [${DEFAULT_SERVER}]: `);
|
|
324
|
+
const serverUrl = serverUrlInput.trim() || DEFAULT_SERVER;
|
|
173
325
|
const email = await ask('Email: ');
|
|
174
326
|
const password = await ask('Password: ');
|
|
175
327
|
try {
|