memory-journal-mcp 7.5.0 → 7.6.1

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/cli.js CHANGED
@@ -1,26 +1,31 @@
1
- import { VERSION, createServer } from './chunk-XNOUTCRV.js';
2
- import { DEFAULT_AUDIT_LOG_MAX_SIZE_BYTES } from './chunk-VHA46GLM.js';
3
- import './chunk-OKOVZ5QE.js';
4
- import { logger } from './chunk-WXDEVIFL.js';
1
+ import { VERSION, DEFAULT_AUDIT_LOG_MAX_SIZE_BYTES, createServer } from './chunk-DFQG7UR5.js';
2
+ import { logger } from './chunk-SYTB5YNH.js';
5
3
  import { Command } from 'commander';
4
+ import * as path from 'path';
6
5
  import * as fs from 'fs';
6
+ import { z } from 'zod';
7
7
 
8
- function resolveDbPath(envPath, defaultName, testName) {
8
+ function parseConfigIntRequired(value, name, min, max) {
9
+ const parsed = parseInt(value, 10);
10
+ if (Number.isNaN(parsed)) {
11
+ throw new Error(`Invalid required numeric configuration for ${name}: ${value}`);
12
+ }
13
+ if (min !== void 0 && parsed < min) {
14
+ throw new Error(`Configuration ${name} must be at least ${min}`);
15
+ }
16
+ if (max !== void 0 && parsed > max) {
17
+ throw new Error(`Configuration ${name} must be at most ${max}`);
18
+ }
19
+ return parsed;
20
+ }
21
+ function resolveDbPath(envPath, defaultName) {
9
22
  if (envPath) return envPath;
10
- const rootPath = `./${defaultName}`;
11
- const testPath = `./test-server/${testName}`;
12
- if (fs.existsSync(rootPath)) return rootPath;
13
- if (fs.existsSync(testPath)) return testPath;
14
- return rootPath;
23
+ return `./${defaultName}`;
15
24
  }
16
- var defaultDbPath = resolveDbPath(
17
- process.env["DB_PATH"],
18
- "memory_journal.db",
19
- "test-memory-journal.db"
20
- );
25
+ var defaultDbPath = resolveDbPath(process.env["DB_PATH"], "memory_journal.db");
21
26
  var defaultTeamDbPath = process.env["TEAM_DB_PATH"] ? process.env["TEAM_DB_PATH"] : void 0;
22
27
  var program = new Command();
23
- program.name("memory-journal-mcp").description("Project context management for AI-assisted development").version(VERSION).option("--transport <type>", "Transport type: stdio or http", "stdio").option("--port <number>", "HTTP port (for http transport)", "3000").option("--server-host <host>", "Server bind host for HTTP transport (default: localhost)").option("--stateless", "Use stateless HTTP mode (no session management)").option("--db <path>", "Database path (env: DB_PATH)", defaultDbPath).option("--team-db <path>", "Team database path (env: TEAM_DB_PATH)", defaultTeamDbPath).option("--tool-filter <filter>", 'Tool filter string (e.g., "starter", "core,search")').option("--default-project <number>", "Default GitHub Project number").option("--auto-rebuild-index", "Rebuild vector index on server startup").option("--cors-origin <origin>", "CORS allowed origin for HTTP transport (default: *)").option("--enable-hsts", "Enable HSTS header for HTTP transport (use when behind HTTPS)").option(
28
+ program.name("memory-journal-mcp").description("Project context management for AI-assisted development").version(VERSION).option("--transport <type>", "Transport type: stdio or http", "stdio").option("--port <number>", "HTTP port (for http transport)", "3000").option("--server-host <host>", "Server bind host for HTTP transport (default: localhost)").option("--stateless", "Use stateless HTTP mode (no session management)").option("--db <path>", "Database path (env: DB_PATH)", defaultDbPath).option("--team-db <path>", "Team database path (env: TEAM_DB_PATH)", defaultTeamDbPath).option("--tool-filter <filter>", 'Tool filter string (e.g., "starter", "core,search")').option("--default-project <number>", "Default GitHub Project number").option("--auto-rebuild-index", "Rebuild vector index on server startup").option("--cors-origin <origin>", "CORS allowed origin for HTTP transport (default: none)").option("--enable-hsts", "Enable HSTS header for HTTP transport (use when behind HTTPS)").option(
24
29
  "--auth-token <token>",
25
30
  "Bearer token for HTTP transport authentication (env: MCP_AUTH_TOKEN)"
26
31
  ).option("--log-level <level>", "Log level: debug, info, warning, error", "info").option(
@@ -39,10 +44,6 @@ program.name("memory-journal-mcp").description("Project context management for A
39
44
  "--digest-interval <minutes>",
40
45
  "Analytics digest interval in minutes, HTTP only (0 = disabled; recommended: 1440 for daily)",
41
46
  "0"
42
- ).option(
43
- "--sandbox-mode <mode>",
44
- 'Code Mode sandbox: "worker" (production, default) or "vm" (lightweight)',
45
- "worker"
46
47
  ).option(
47
48
  "--codemode-max-result-size <bytes>",
48
49
  "Maximum Code Mode result size in bytes (default: 102400 / 100KB, env: CODE_MODE_MAX_RESULT_SIZE)"
@@ -50,10 +51,19 @@ program.name("memory-journal-mcp").description("Project context management for A
50
51
  "--oauth-clock-tolerance <seconds>",
51
52
  "OAuth clock tolerance in seconds (default: 60)",
52
53
  "60"
54
+ ).option(
55
+ "--oauth-allow-plaintext-loopback",
56
+ "Allow plaintext loopback OAuth issuer (env: OAUTH_ALLOW_PLAINTEXT_LOOPBACK)"
57
+ ).option("--trust-proxy", "Trust reverse proxy headers (e.g. X-Forwarded-For env: TRUST_PROXY)").option(
58
+ "--public-origin <url>",
59
+ "Public origin URL for webhook verification and OAuth redirects (env: PUBLIC_ORIGIN)"
53
60
  ).option(
54
61
  "--audit-log <path>",
55
62
  'Enable audit logging to the specified JSONL file path, or "stderr" for container mode (env: AUDIT_LOG_PATH)'
56
- ).option("--audit-redact", "Redact tool arguments from audit entries (env: AUDIT_REDACT)").option(
63
+ ).option(
64
+ "--no-audit-redact",
65
+ "Disable redaction of tool arguments from audit entries (env: AUDIT_REDACT=false)"
66
+ ).option(
57
67
  "--audit-reads",
58
68
  "Enable audit logging for read-scoped tool calls (default: off, env: AUDIT_READS)"
59
69
  ).option(
@@ -64,6 +74,12 @@ program.name("memory-journal-mcp").description("Project context management for A
64
74
  "--instruction-level <level>",
65
75
  "Briefing depth: essential, standard, full (env: INSTRUCTION_LEVEL)",
66
76
  "standard"
77
+ ).option(
78
+ "--allowed-io-roots <paths>",
79
+ "Comma-separated absolute paths or JSON array of paths for strict filesystem jailing (env: ALLOWED_IO_ROOTS)"
80
+ ).option(
81
+ "--codemode-internal-full-access",
82
+ "Bypass tool filter constraints within the Code Mode sandbox (env: CODEMODE_INTERNAL_FULL_ACCESS)"
67
83
  ).option(
68
84
  "--briefing-entries <count>",
69
85
  "Number of journal entries in briefing (env: BRIEFING_ENTRY_COUNT)",
@@ -115,6 +131,21 @@ program.name("memory-journal-mcp").description("Project context management for A
115
131
  ).action(
116
132
  async (options) => {
117
133
  logger.setLevel(options.logLevel);
134
+ const sensitiveEnvVars = [
135
+ "GITHUB_TOKEN",
136
+ "MCP_AUTH_TOKEN",
137
+ "OAUTH_ISSUER",
138
+ "OAUTH_JWKS_URI"
139
+ ];
140
+ for (const envVar of sensitiveEnvVars) {
141
+ if (process.env[envVar]?.startsWith("CHANGEME_")) {
142
+ logger.error(
143
+ `FATAL: Insecure configuration detected. ${envVar} contains default placeholder value. Please update your environment variables.`,
144
+ { module: "CLI" }
145
+ );
146
+ process.exit(1);
147
+ }
148
+ }
118
149
  const host = options.serverHost ?? process.env["MCP_HOST"] ?? process.env["HOST"] ?? void 0;
119
150
  logger.info("Starting Memory Journal MCP Server", {
120
151
  module: "CLI",
@@ -133,91 +164,236 @@ program.name("memory-journal-mcp").description("Project context management for A
133
164
  const auditConfig = auditLogPath ? {
134
165
  enabled: true,
135
166
  logPath: auditLogPath,
136
- redact: options.auditRedact ?? process.env["AUDIT_REDACT"] === "true",
167
+ redact: options.auditRedact ?? (process.env["AUDIT_REDACT"] ? process.env["AUDIT_REDACT"] === "true" : true),
137
168
  auditReads: options.auditReads ?? process.env["AUDIT_READS"] === "true",
138
- maxSizeBytes: parseInt(
169
+ maxSizeBytes: parseConfigIntRequired(
139
170
  process.env["AUDIT_LOG_MAX_SIZE"] ?? options.auditLogMaxSize,
140
- 10
171
+ "audit-log-max-size",
172
+ 1024
141
173
  )
142
174
  } : void 0;
143
175
  await createServer({
144
176
  transport: options.transport,
145
- port: parseInt(options.port, 10),
177
+ port: parseConfigIntRequired(options.port, "port", 1, 65535),
146
178
  host,
147
179
  statelessHttp: options.stateless === true,
148
180
  dbPath: options.db,
149
181
  teamDbPath: options.teamDb,
150
182
  toolFilter: options.toolFilter,
151
- defaultProjectNumber: options.defaultProject ? parseInt(options.defaultProject, 10) : process.env["DEFAULT_PROJECT_NUMBER"] ? parseInt(process.env["DEFAULT_PROJECT_NUMBER"], 10) : void 0,
183
+ defaultProjectNumber: options.defaultProject ? parseConfigIntRequired(options.defaultProject, "default-project", 1) : process.env["DEFAULT_PROJECT_NUMBER"] ? parseConfigIntRequired(
184
+ process.env["DEFAULT_PROJECT_NUMBER"],
185
+ "DEFAULT_PROJECT_NUMBER",
186
+ 1
187
+ ) : void 0,
152
188
  autoRebuildIndex: options.autoRebuildIndex ?? process.env["AUTO_REBUILD_INDEX"] === "true",
153
189
  corsOrigins: options.corsOrigin ? options.corsOrigin.split(",").map((s) => s.trim()) : void 0,
154
190
  enableHSTS: options.enableHsts ?? process.env["MCP_ENABLE_HSTS"] === "true",
155
191
  authToken: options.authToken,
156
192
  scheduler: {
157
- backupIntervalMinutes: parseInt(options.backupInterval, 10),
158
- keepBackups: parseInt(options.keepBackups, 10),
159
- vacuumIntervalMinutes: parseInt(options.vacuumInterval, 10),
160
- rebuildIndexIntervalMinutes: parseInt(options.rebuildIndexInterval, 10),
161
- digestIntervalMinutes: parseInt(options.digestInterval, 10)
193
+ backupIntervalMinutes: parseConfigIntRequired(
194
+ options.backupInterval,
195
+ "backup-interval",
196
+ 0
197
+ ),
198
+ keepBackups: parseConfigIntRequired(
199
+ options.keepBackups,
200
+ "keep-backups",
201
+ 1,
202
+ 100
203
+ ),
204
+ vacuumIntervalMinutes: parseConfigIntRequired(
205
+ options.vacuumInterval,
206
+ "vacuum-interval",
207
+ 0
208
+ ),
209
+ rebuildIndexIntervalMinutes: parseConfigIntRequired(
210
+ options.rebuildIndexInterval,
211
+ "rebuild-index-interval",
212
+ 0
213
+ ),
214
+ digestIntervalMinutes: parseConfigIntRequired(
215
+ options.digestInterval,
216
+ "digest-interval",
217
+ 0
218
+ )
162
219
  },
163
- sandboxMode: options.sandboxMode,
164
220
  // OAuth 2.1
165
221
  oauthEnabled: options.oauthEnabled ?? process.env["OAUTH_ENABLED"] === "true",
166
222
  oauthIssuer: options.oauthIssuer ?? process.env["OAUTH_ISSUER"],
167
223
  oauthAudience: options.oauthAudience ?? process.env["OAUTH_AUDIENCE"],
168
224
  oauthJwksUri: options.oauthJwksUri ?? process.env["OAUTH_JWKS_URI"],
169
- oauthClockTolerance: parseInt(
225
+ oauthClockTolerance: parseConfigIntRequired(
170
226
  process.env["OAUTH_CLOCK_TOLERANCE"] ?? options.oauthClockTolerance,
171
- 10
227
+ "oauth-clock-tolerance",
228
+ 0,
229
+ 3600
172
230
  ),
231
+ allowPlaintextLoopbackOAuth: options.oauthAllowPlaintextLoopback ?? process.env["OAUTH_ALLOW_PLAINTEXT_LOOPBACK"] === "true",
232
+ trustProxy: options.trustProxy ?? process.env["TRUST_PROXY"] === "true",
233
+ publicOrigin: options.publicOrigin ?? process.env["PUBLIC_ORIGIN"],
234
+ codemodeInternalFullAccess: options.codemodeInternalFullAccess ?? process.env["CODEMODE_INTERNAL_FULL_ACCESS"] === "true",
173
235
  // Project Registry
174
236
  projectRegistry: (() => {
175
237
  const raw = process.env["PROJECT_REGISTRY"];
176
238
  if (!raw) return void 0;
239
+ if (raw.length > 51200) {
240
+ throw new Error(
241
+ "PROJECT_REGISTRY environment variable exceeds size limit (50KB)"
242
+ );
243
+ }
177
244
  try {
178
- return JSON.parse(raw);
245
+ const registrySchema = z.record(
246
+ z.string(),
247
+ z.object({
248
+ path: z.string().min(1),
249
+ project_number: z.number().nullable().optional()
250
+ }).strict()
251
+ );
252
+ const parsed = JSON.parse(raw);
253
+ const deepClean = (obj) => {
254
+ if (obj === null || typeof obj !== "object") return obj;
255
+ if (Array.isArray(obj)) return obj.map(deepClean);
256
+ const out = {};
257
+ for (const key of Object.keys(obj)) {
258
+ if (key === "__proto__" || key === "constructor" || key === "prototype")
259
+ continue;
260
+ out[key] = deepClean(obj[key]);
261
+ }
262
+ return out;
263
+ };
264
+ const safeParsed = deepClean(parsed);
265
+ const validated = registrySchema.parse(safeParsed);
266
+ for (const key of Object.keys(validated)) {
267
+ const entry = validated[key];
268
+ if (!entry?.path) continue;
269
+ if (!path.isAbsolute(entry.path)) {
270
+ throw new Error(
271
+ `Project registry path must be an absolute path: ${entry.path}`
272
+ );
273
+ }
274
+ const resolvedPath = path.resolve(entry.path);
275
+ try {
276
+ const stat = fs.lstatSync(resolvedPath);
277
+ if (stat.isSymbolicLink()) {
278
+ throw new Error(
279
+ `Project registry path cannot be a symlink (symlink traversal protection): ${resolvedPath}`
280
+ );
281
+ }
282
+ if (!stat.isDirectory()) {
283
+ throw new Error(
284
+ `Project registry path is not a directory: ${resolvedPath}`
285
+ );
286
+ }
287
+ } catch (e) {
288
+ const errMsg = e instanceof Error ? e.message : String(e);
289
+ throw new Error(
290
+ `Project registry path does not exist or cannot be accessed: ${resolvedPath} (${errMsg})`,
291
+ { cause: e }
292
+ );
293
+ }
294
+ entry.path = resolvedPath;
295
+ }
296
+ return validated;
179
297
  } catch (e) {
180
298
  const errName = e instanceof Error ? e.message : String(e);
181
299
  throw new Error(
182
- `Failed to parse PROJECT_REGISTRY environment variable. Must be valid JSON: ${errName}`,
300
+ `Failed to parse PROJECT_REGISTRY environment variable. Must be valid JSON and safe paths: ${errName}`,
183
301
  { cause: e }
184
302
  );
185
303
  }
186
304
  })(),
305
+ // Allowed IO Roots
306
+ allowedIoRoots: (() => {
307
+ const raw = options.allowedIoRoots ?? process.env["ALLOWED_IO_ROOTS"];
308
+ if (!raw) return void 0;
309
+ if (raw.length > 51200) {
310
+ throw new Error(
311
+ "ALLOWED_IO_ROOTS configuration exceeds size limit (50KB)"
312
+ );
313
+ }
314
+ try {
315
+ if (raw.trim().startsWith("[")) {
316
+ const parsed = JSON.parse(raw);
317
+ if (Array.isArray(parsed) && parsed.every((p) => typeof p === "string" && path.isAbsolute(p))) {
318
+ const result = parsed;
319
+ for (const p of result) {
320
+ try {
321
+ if (!fs.existsSync(p)) {
322
+ console.warn(
323
+ `
324
+ [WARN] ALLOWED_IO_ROOTS path does not exist: ${p}
325
+ `
326
+ );
327
+ }
328
+ } catch {
329
+ }
330
+ }
331
+ return result;
332
+ }
333
+ throw new Error("Must be an array of absolute paths");
334
+ }
335
+ const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
336
+ if (parts.some((p) => !path.isAbsolute(p))) {
337
+ throw new Error("All paths must be absolute");
338
+ }
339
+ for (const p of parts) {
340
+ try {
341
+ if (!fs.existsSync(p)) {
342
+ console.warn(
343
+ `
344
+ [WARN] ALLOWED_IO_ROOTS path does not exist: ${p}
345
+ `
346
+ );
347
+ }
348
+ } catch {
349
+ }
350
+ }
351
+ return parts;
352
+ } catch (e) {
353
+ const errName = e instanceof Error ? e.message : String(e);
354
+ throw new Error(`Invalid ALLOWED_IO_ROOTS configuration: ${errName}`, {
355
+ cause: e
356
+ });
357
+ }
358
+ })(),
187
359
  // Briefing configuration
188
360
  briefingConfig: {
189
- entryCount: parseInt(
361
+ entryCount: parseConfigIntRequired(
190
362
  process.env["BRIEFING_ENTRY_COUNT"] ?? options.briefingEntries,
191
- 10
363
+ "briefing-entries"
192
364
  ),
193
- summaryCount: parseInt(
365
+ summaryCount: parseConfigIntRequired(
194
366
  process.env["BRIEFING_SUMMARY_COUNT"] ?? options.briefingSummaries,
195
- 10
367
+ "briefing-summaries"
196
368
  ),
197
369
  includeTeam: options.briefingIncludeTeam ?? process.env["BRIEFING_INCLUDE_TEAM"] === "true",
198
- issueCount: parseInt(
370
+ issueCount: parseConfigIntRequired(
199
371
  process.env["BRIEFING_ISSUE_COUNT"] ?? options.briefingIssues,
200
- 10
372
+ "briefing-issues"
201
373
  ),
202
- prCount: parseInt(
374
+ prCount: parseConfigIntRequired(
203
375
  process.env["BRIEFING_PR_COUNT"] ?? options.briefingPrs,
204
- 10
376
+ "briefing-prs"
205
377
  ),
206
378
  prStatusBreakdown: options.briefingPrStatus ?? process.env["BRIEFING_PR_STATUS"] === "true",
207
- milestoneCount: parseInt(
379
+ milestoneCount: parseConfigIntRequired(
208
380
  process.env["BRIEFING_MILESTONE_COUNT"] ?? options.briefingMilestones,
209
- 10
381
+ "briefing-milestones"
210
382
  ),
211
- rulesFilePath: options.rulesFile ?? process.env["RULES_FILE_PATH"] ?? void 0,
212
- skillsDirPath: options.skillsDir ?? process.env["SKILLS_DIR_PATH"] ?? void 0,
213
- workflowCount: parseInt(
383
+ rulesFilePath: options.rulesFile ? path.resolve(process.cwd(), options.rulesFile) : process.env["RULES_FILE_PATH"] ? path.resolve(process.cwd(), process.env["RULES_FILE_PATH"]) : void 0,
384
+ skillsDirPath: options.skillsDir ? path.resolve(process.cwd(), options.skillsDir) : process.env["SKILLS_DIR_PATH"] ? path.resolve(process.cwd(), process.env["SKILLS_DIR_PATH"]) : void 0,
385
+ workflowCount: parseConfigIntRequired(
214
386
  process.env["BRIEFING_WORKFLOW_COUNT"] ?? options.briefingWorkflows,
215
- 10
387
+ "briefing-workflows"
216
388
  ),
217
389
  workflowStatusBreakdown: options.briefingWorkflowStatus ?? process.env["BRIEFING_WORKFLOW_STATUS"] === "true",
218
390
  copilotReviews: options.briefingCopilot ?? process.env["BRIEFING_COPILOT_REVIEWS"] === "true",
219
391
  workflowSummary: options.workflowSummary ?? process.env["MEMORY_JOURNAL_WORKFLOW_SUMMARY"] ?? void 0,
220
- defaultProjectNumber: options.defaultProject ? parseInt(options.defaultProject, 10) : process.env["DEFAULT_PROJECT_NUMBER"] ? parseInt(process.env["DEFAULT_PROJECT_NUMBER"], 10) : void 0
392
+ defaultProjectNumber: options.defaultProject ? parseConfigIntRequired(options.defaultProject, "default-project", 1) : process.env["DEFAULT_PROJECT_NUMBER"] ? parseConfigIntRequired(
393
+ process.env["DEFAULT_PROJECT_NUMBER"],
394
+ "DEFAULT_PROJECT_NUMBER",
395
+ 1
396
+ ) : void 0
221
397
  },
222
398
  instructionLevel: options.instructionLevel !== "standard" ? options.instructionLevel : process.env["INSTRUCTION_LEVEL"] ?? "standard",
223
399
  auditConfig,