memory-journal-mcp 7.4.0 → 7.6.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/cli.js CHANGED
@@ -1,26 +1,31 @@
1
- import { VERSION, createServer } from './chunk-5ZA77VUW.js';
2
- import { DEFAULT_AUDIT_LOG_MAX_SIZE_BYTES } from './chunk-P5V2VY6N.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-NSEHC6MZ.js';
2
+ import { logger } from './chunk-SV3CKPMF.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)",
@@ -109,9 +125,27 @@ program.name("memory-journal-mcp").description("Project context management for A
109
125
  ).option(
110
126
  "--workflow-summary <text>",
111
127
  "Workflow summary for memory://workflows resource (env: MEMORY_JOURNAL_WORKFLOW_SUMMARY)"
128
+ ).option(
129
+ "--flag-vocabulary <terms>",
130
+ "Comma-separated flag vocabulary for Hush Protocol (env: FLAG_VOCABULARY, default: blocker,needs_review,help_requested,fyi)"
112
131
  ).action(
113
132
  async (options) => {
114
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
+ }
115
149
  const host = options.serverHost ?? process.env["MCP_HOST"] ?? process.env["HOST"] ?? void 0;
116
150
  logger.info("Starting Memory Journal MCP Server", {
117
151
  module: "CLI",
@@ -130,94 +164,244 @@ program.name("memory-journal-mcp").description("Project context management for A
130
164
  const auditConfig = auditLogPath ? {
131
165
  enabled: true,
132
166
  logPath: auditLogPath,
133
- redact: options.auditRedact ?? process.env["AUDIT_REDACT"] === "true",
167
+ redact: options.auditRedact ?? (process.env["AUDIT_REDACT"] ? process.env["AUDIT_REDACT"] === "true" : true),
134
168
  auditReads: options.auditReads ?? process.env["AUDIT_READS"] === "true",
135
- maxSizeBytes: parseInt(
169
+ maxSizeBytes: parseConfigIntRequired(
136
170
  process.env["AUDIT_LOG_MAX_SIZE"] ?? options.auditLogMaxSize,
137
- 10
171
+ "audit-log-max-size",
172
+ 1024
138
173
  )
139
174
  } : void 0;
140
175
  await createServer({
141
176
  transport: options.transport,
142
- port: parseInt(options.port, 10),
177
+ port: parseConfigIntRequired(options.port, "port", 1, 65535),
143
178
  host,
144
179
  statelessHttp: options.stateless === true,
145
180
  dbPath: options.db,
146
181
  teamDbPath: options.teamDb,
147
182
  toolFilter: options.toolFilter,
148
- 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,
149
188
  autoRebuildIndex: options.autoRebuildIndex ?? process.env["AUTO_REBUILD_INDEX"] === "true",
150
189
  corsOrigins: options.corsOrigin ? options.corsOrigin.split(",").map((s) => s.trim()) : void 0,
151
190
  enableHSTS: options.enableHsts ?? process.env["MCP_ENABLE_HSTS"] === "true",
152
191
  authToken: options.authToken,
153
192
  scheduler: {
154
- backupIntervalMinutes: parseInt(options.backupInterval, 10),
155
- keepBackups: parseInt(options.keepBackups, 10),
156
- vacuumIntervalMinutes: parseInt(options.vacuumInterval, 10),
157
- rebuildIndexIntervalMinutes: parseInt(options.rebuildIndexInterval, 10),
158
- 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
+ )
159
219
  },
160
- sandboxMode: options.sandboxMode,
161
220
  // OAuth 2.1
162
221
  oauthEnabled: options.oauthEnabled ?? process.env["OAUTH_ENABLED"] === "true",
163
222
  oauthIssuer: options.oauthIssuer ?? process.env["OAUTH_ISSUER"],
164
223
  oauthAudience: options.oauthAudience ?? process.env["OAUTH_AUDIENCE"],
165
224
  oauthJwksUri: options.oauthJwksUri ?? process.env["OAUTH_JWKS_URI"],
166
- oauthClockTolerance: parseInt(
225
+ oauthClockTolerance: parseConfigIntRequired(
167
226
  process.env["OAUTH_CLOCK_TOLERANCE"] ?? options.oauthClockTolerance,
168
- 10
227
+ "oauth-clock-tolerance",
228
+ 0,
229
+ 3600
169
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",
170
235
  // Project Registry
171
236
  projectRegistry: (() => {
172
237
  const raw = process.env["PROJECT_REGISTRY"];
173
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
+ }
174
244
  try {
175
- 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;
176
297
  } catch (e) {
177
298
  const errName = e instanceof Error ? e.message : String(e);
178
299
  throw new Error(
179
- `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}`,
180
301
  { cause: e }
181
302
  );
182
303
  }
183
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
+ })(),
184
359
  // Briefing configuration
185
360
  briefingConfig: {
186
- entryCount: parseInt(
361
+ entryCount: parseConfigIntRequired(
187
362
  process.env["BRIEFING_ENTRY_COUNT"] ?? options.briefingEntries,
188
- 10
363
+ "briefing-entries"
189
364
  ),
190
- summaryCount: parseInt(
365
+ summaryCount: parseConfigIntRequired(
191
366
  process.env["BRIEFING_SUMMARY_COUNT"] ?? options.briefingSummaries,
192
- 10
367
+ "briefing-summaries"
193
368
  ),
194
369
  includeTeam: options.briefingIncludeTeam ?? process.env["BRIEFING_INCLUDE_TEAM"] === "true",
195
- issueCount: parseInt(
370
+ issueCount: parseConfigIntRequired(
196
371
  process.env["BRIEFING_ISSUE_COUNT"] ?? options.briefingIssues,
197
- 10
372
+ "briefing-issues"
198
373
  ),
199
- prCount: parseInt(
374
+ prCount: parseConfigIntRequired(
200
375
  process.env["BRIEFING_PR_COUNT"] ?? options.briefingPrs,
201
- 10
376
+ "briefing-prs"
202
377
  ),
203
378
  prStatusBreakdown: options.briefingPrStatus ?? process.env["BRIEFING_PR_STATUS"] === "true",
204
- milestoneCount: parseInt(
379
+ milestoneCount: parseConfigIntRequired(
205
380
  process.env["BRIEFING_MILESTONE_COUNT"] ?? options.briefingMilestones,
206
- 10
381
+ "briefing-milestones"
207
382
  ),
208
- rulesFilePath: options.rulesFile ?? process.env["RULES_FILE_PATH"] ?? void 0,
209
- skillsDirPath: options.skillsDir ?? process.env["SKILLS_DIR_PATH"] ?? void 0,
210
- 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(
211
386
  process.env["BRIEFING_WORKFLOW_COUNT"] ?? options.briefingWorkflows,
212
- 10
387
+ "briefing-workflows"
213
388
  ),
214
389
  workflowStatusBreakdown: options.briefingWorkflowStatus ?? process.env["BRIEFING_WORKFLOW_STATUS"] === "true",
215
390
  copilotReviews: options.briefingCopilot ?? process.env["BRIEFING_COPILOT_REVIEWS"] === "true",
216
391
  workflowSummary: options.workflowSummary ?? process.env["MEMORY_JOURNAL_WORKFLOW_SUMMARY"] ?? void 0,
217
- 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
218
397
  },
219
398
  instructionLevel: options.instructionLevel !== "standard" ? options.instructionLevel : process.env["INSTRUCTION_LEVEL"] ?? "standard",
220
- auditConfig
399
+ auditConfig,
400
+ flagVocabulary: (() => {
401
+ const raw = options.flagVocabulary ?? process.env["FLAG_VOCABULARY"];
402
+ if (!raw) return void 0;
403
+ return raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
404
+ })()
221
405
  });
222
406
  } catch (error) {
223
407
  logger.error("Failed to start server", {