@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.
Files changed (2) hide show
  1. package/dist/index.js +166 -14
  2. 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.1.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.string().describe('Project name or git remote URL'),
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.string().optional().describe('Session ID for grouping'),
23
- }, async ({ project, type, content, tags, sessionId }) => {
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: [{ type: 'text', text: `Logged ${type} entry.` }],
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.string().describe('Project name or git remote URL'),
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 "${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 serverUrl = await ask('Server URL (e.g. https://forever-z44hy.ondigitalocean.app): ');
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squidcode/forever-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP plugin for Forever - Claude Memory System",
5
5
  "type": "module",
6
6
  "bin": {