gossipcat 0.4.19 → 0.4.20
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-mcp/mcp-server.js +60 -8
- package/docs/HANDBOOK.md +19 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +100 -11
package/dist-mcp/mcp-server.js
CHANGED
|
@@ -51,6 +51,7 @@ var init_mcp_context = __esm({
|
|
|
51
51
|
skillEngine: null,
|
|
52
52
|
nativeTaskMap: /* @__PURE__ */ new Map(),
|
|
53
53
|
nativeResultMap: /* @__PURE__ */ new Map(),
|
|
54
|
+
nativeUtilityResultMap: /* @__PURE__ */ new Map(),
|
|
54
55
|
nativeAgentConfigs: /* @__PURE__ */ new Map(),
|
|
55
56
|
identityRegistry: /* @__PURE__ */ new Map(),
|
|
56
57
|
pendingConsensusRounds: /* @__PURE__ */ new Map(),
|
|
@@ -13675,7 +13676,10 @@ function extractFrontmatterField(content, field) {
|
|
|
13675
13676
|
if (colonIdx === -1) continue;
|
|
13676
13677
|
const key = line.slice(0, colonIdx).trim();
|
|
13677
13678
|
if (key !== field) continue;
|
|
13678
|
-
|
|
13679
|
+
let value = line.slice(colonIdx + 1).trim();
|
|
13680
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
13681
|
+
value = value.slice(1, -1);
|
|
13682
|
+
}
|
|
13679
13683
|
return value.length > 0 ? value : null;
|
|
13680
13684
|
}
|
|
13681
13685
|
return null;
|
|
@@ -16545,7 +16549,8 @@ ${safeSnippet}
|
|
|
16545
16549
|
const trailing = relSegs.slice(relSegs.length - refSegs.length);
|
|
16546
16550
|
return trailing.every((seg, i) => seg === refSegs[i]);
|
|
16547
16551
|
}
|
|
16548
|
-
async findFile(dir, fileName, validRoots, anchorRoot, fileRef) {
|
|
16552
|
+
async findFile(dir, fileName, validRoots, anchorRoot, fileRef, depth = 0) {
|
|
16553
|
+
if (depth >= 10) return null;
|
|
16549
16554
|
try {
|
|
16550
16555
|
const entries = await (0, import_promises3.readdir)(dir, { withFileTypes: true });
|
|
16551
16556
|
for (const entry of entries) {
|
|
@@ -16566,7 +16571,7 @@ ${safeSnippet}
|
|
|
16566
16571
|
return real;
|
|
16567
16572
|
}
|
|
16568
16573
|
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
|
|
16569
|
-
const found = await this.findFile(fullPath, fileName, validRoots, anchorRoot, fileRef);
|
|
16574
|
+
const found = await this.findFile(fullPath, fileName, validRoots, anchorRoot, fileRef, depth + 1);
|
|
16570
16575
|
if (found) return found;
|
|
16571
16576
|
}
|
|
16572
16577
|
}
|
|
@@ -25107,6 +25112,18 @@ var init_auth = __esm({
|
|
|
25107
25112
|
}
|
|
25108
25113
|
});
|
|
25109
25114
|
|
|
25115
|
+
// packages/relay/src/dashboard/utility-agents.ts
|
|
25116
|
+
function isUtilityAgent(agentId) {
|
|
25117
|
+
return UTILITY_AGENT_IDS.has(agentId);
|
|
25118
|
+
}
|
|
25119
|
+
var UTILITY_AGENT_IDS;
|
|
25120
|
+
var init_utility_agents = __esm({
|
|
25121
|
+
"packages/relay/src/dashboard/utility-agents.ts"() {
|
|
25122
|
+
"use strict";
|
|
25123
|
+
UTILITY_AGENT_IDS = /* @__PURE__ */ new Set(["_utility"]);
|
|
25124
|
+
}
|
|
25125
|
+
});
|
|
25126
|
+
|
|
25110
25127
|
// packages/relay/src/dashboard/api-overview.ts
|
|
25111
25128
|
function readSkillStatus(path2) {
|
|
25112
25129
|
try {
|
|
@@ -25212,6 +25229,7 @@ async function overviewHandler(projectRoot, ctx2) {
|
|
|
25212
25229
|
try {
|
|
25213
25230
|
const ev = JSON.parse(line);
|
|
25214
25231
|
if (ev.type === "task.created") {
|
|
25232
|
+
if (ev.agentId && isUtilityAgent(ev.agentId)) continue;
|
|
25215
25233
|
if (ev.taskId && ev.agentId) {
|
|
25216
25234
|
created.set(ev.taskId, { agentId: ev.agentId, timestamp: ev.timestamp || "" });
|
|
25217
25235
|
}
|
|
@@ -25318,6 +25336,7 @@ var init_api_overview = __esm({
|
|
|
25318
25336
|
"use strict";
|
|
25319
25337
|
import_fs42 = require("fs");
|
|
25320
25338
|
import_path45 = require("path");
|
|
25339
|
+
init_utility_agents();
|
|
25321
25340
|
}
|
|
25322
25341
|
});
|
|
25323
25342
|
|
|
@@ -25504,6 +25523,7 @@ function readTaskGraphByAgent(projectRoot) {
|
|
|
25504
25523
|
}
|
|
25505
25524
|
for (const [taskId, createdData] of created) {
|
|
25506
25525
|
const { agentId, task, timestamp } = createdData;
|
|
25526
|
+
if (isUtilityAgent(agentId)) continue;
|
|
25507
25527
|
if (!result.has(agentId)) {
|
|
25508
25528
|
result.set(agentId, { totalTokens: 0, lastTask: null });
|
|
25509
25529
|
}
|
|
@@ -25620,6 +25640,7 @@ var init_api_agents = __esm({
|
|
|
25620
25640
|
init_skill_index();
|
|
25621
25641
|
import_fs44 = require("fs");
|
|
25622
25642
|
import_path47 = require("path");
|
|
25643
|
+
init_utility_agents();
|
|
25623
25644
|
DEFAULT_SCORE = {
|
|
25624
25645
|
agentId: "",
|
|
25625
25646
|
accuracy: 0.5,
|
|
@@ -26292,6 +26313,7 @@ async function tasksHandler(projectRoot, query) {
|
|
|
26292
26313
|
}
|
|
26293
26314
|
const tasks = [];
|
|
26294
26315
|
for (const [taskId, info] of created) {
|
|
26316
|
+
if (isUtilityAgent(info.agentId)) continue;
|
|
26295
26317
|
const result = completed.get(taskId);
|
|
26296
26318
|
tasks.push({
|
|
26297
26319
|
taskId,
|
|
@@ -26314,6 +26336,7 @@ var init_api_tasks = __esm({
|
|
|
26314
26336
|
"use strict";
|
|
26315
26337
|
import_fs53 = require("fs");
|
|
26316
26338
|
import_path56 = require("path");
|
|
26339
|
+
init_utility_agents();
|
|
26317
26340
|
}
|
|
26318
26341
|
});
|
|
26319
26342
|
|
|
@@ -26329,6 +26352,7 @@ async function activeTasksHandler(projectRoot) {
|
|
|
26329
26352
|
try {
|
|
26330
26353
|
const ev = JSON.parse(line);
|
|
26331
26354
|
if (ev.type === "task.created" && ev.taskId) {
|
|
26355
|
+
if (isUtilityAgent(ev.agentId)) continue;
|
|
26332
26356
|
created.set(ev.taskId, { agentId: ev.agentId || "", task: ev.task || "", timestamp: ev.timestamp || "" });
|
|
26333
26357
|
} else if (ev.type === "task.completed" || ev.type === "task.failed" || ev.type === "task.cancelled") {
|
|
26334
26358
|
finished.add(ev.taskId);
|
|
@@ -26357,6 +26381,7 @@ var init_api_active_tasks = __esm({
|
|
|
26357
26381
|
"use strict";
|
|
26358
26382
|
import_fs54 = require("fs");
|
|
26359
26383
|
import_path57 = require("path");
|
|
26384
|
+
init_utility_agents();
|
|
26360
26385
|
}
|
|
26361
26386
|
});
|
|
26362
26387
|
|
|
@@ -43403,6 +43428,14 @@ function recordTimeoutSignal(taskId, agentId) {
|
|
|
43403
43428
|
} catch {
|
|
43404
43429
|
}
|
|
43405
43430
|
}
|
|
43431
|
+
var UTILITY_RESULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
43432
|
+
function scheduleNativeTaskEviction(intervalMs = 60 * 60 * 1e3) {
|
|
43433
|
+
const timer = setInterval(evictStaleNativeTasks, intervalMs);
|
|
43434
|
+
timer.unref();
|
|
43435
|
+
return {
|
|
43436
|
+
stop: () => clearInterval(timer)
|
|
43437
|
+
};
|
|
43438
|
+
}
|
|
43406
43439
|
function evictStaleNativeTasks() {
|
|
43407
43440
|
const now = Date.now();
|
|
43408
43441
|
let changed = false;
|
|
@@ -43418,6 +43451,11 @@ function evictStaleNativeTasks() {
|
|
|
43418
43451
|
changed = true;
|
|
43419
43452
|
}
|
|
43420
43453
|
}
|
|
43454
|
+
for (const [id, info] of [...ctx.nativeUtilityResultMap]) {
|
|
43455
|
+
if (now - info.startedAt > UTILITY_RESULT_TTL_MS) {
|
|
43456
|
+
ctx.nativeUtilityResultMap.delete(id);
|
|
43457
|
+
}
|
|
43458
|
+
}
|
|
43421
43459
|
if (changed) persistNativeTaskMap();
|
|
43422
43460
|
}
|
|
43423
43461
|
function persistNativeTaskMap() {
|
|
@@ -43588,7 +43626,7 @@ async function handleNativeRelay(task_id, result, error48, agentStartedAt, relay
|
|
|
43588
43626
|
const effectiveError = auditBlockError || error48;
|
|
43589
43627
|
const effectiveResult = auditBlockError ? void 0 : error48 ? void 0 : result ? (auditPrefix + result).slice(0, 5e4) : result;
|
|
43590
43628
|
ctx.nativeTaskMap.delete(task_id);
|
|
43591
|
-
|
|
43629
|
+
const resultRecord = {
|
|
43592
43630
|
id: task_id,
|
|
43593
43631
|
agentId: taskInfo.agentId,
|
|
43594
43632
|
task: taskInfo.task,
|
|
@@ -43597,7 +43635,12 @@ async function handleNativeRelay(task_id, result, error48, agentStartedAt, relay
|
|
|
43597
43635
|
error: effectiveError || void 0,
|
|
43598
43636
|
startedAt: effectiveStart,
|
|
43599
43637
|
completedAt: Date.now()
|
|
43600
|
-
}
|
|
43638
|
+
};
|
|
43639
|
+
if (taskInfo.utilityType === "skill_develop") {
|
|
43640
|
+
ctx.nativeUtilityResultMap.set(task_id, resultRecord);
|
|
43641
|
+
} else {
|
|
43642
|
+
ctx.nativeResultMap.set(task_id, resultRecord);
|
|
43643
|
+
}
|
|
43601
43644
|
if (auditBlockError) error48 = auditBlockError;
|
|
43602
43645
|
persistNativeTaskMap();
|
|
43603
43646
|
evictStaleNativeTasks();
|
|
@@ -43634,8 +43677,9 @@ async function handleNativeRelay(task_id, result, error48, agentStartedAt, relay
|
|
|
43634
43677
|
} catch {
|
|
43635
43678
|
}
|
|
43636
43679
|
}
|
|
43680
|
+
const completionResult = taskInfo.utilityType === "skill_develop" ? `[utility] ${taskInfo.task} \u2192 ${(result || "").length} chars` : result;
|
|
43637
43681
|
try {
|
|
43638
|
-
ctx.mainAgent.recordNativeTaskCompleted(task_id,
|
|
43682
|
+
ctx.mainAgent.recordNativeTaskCompleted(task_id, completionResult, error48 || void 0, elapsed ?? void 0, taskInfo.memoryQueryCalled);
|
|
43639
43683
|
} catch {
|
|
43640
43684
|
}
|
|
43641
43685
|
if (taskInfo.writeMode && !taskInfo.utilityType && agentId !== "_utility") {
|
|
@@ -43723,7 +43767,7 @@ async function handleNativeRelay(task_id, result, error48, agentStartedAt, relay
|
|
|
43723
43767
|
}
|
|
43724
43768
|
if (!error48 && taskInfo.utilityType) {
|
|
43725
43769
|
const utilityLabel = taskInfo.utilityType === "summary" ? "cognitive-summary" : taskInfo.utilityType === "gossip" ? "gossip-publish" : taskInfo.utilityType;
|
|
43726
|
-
const utilDurationLabel = elapsed !== null ? `${(elapsed / 1e3).toFixed(1)}s` : "duration=unknown";
|
|
43770
|
+
const utilDurationLabel = elapsed !== null ? `${(elapsed / 1e3).toFixed(1)}s since dispatch` : "duration=unknown";
|
|
43727
43771
|
process.stderr.write(`[gossipcat] \u2705 utility \u2190 ${utilityLabel} [${task_id}] OK (${utilDurationLabel})
|
|
43728
43772
|
`);
|
|
43729
43773
|
}
|
|
@@ -45707,9 +45751,11 @@ async function doBoot() {
|
|
|
45707
45751
|
};
|
|
45708
45752
|
process.once("exit", cleanupPid);
|
|
45709
45753
|
let shuttingDown = false;
|
|
45754
|
+
const eviction = scheduleNativeTaskEviction();
|
|
45710
45755
|
process.once("SIGTERM", async () => {
|
|
45711
45756
|
if (shuttingDown) return;
|
|
45712
45757
|
shuttingDown = true;
|
|
45758
|
+
eviction.stop();
|
|
45713
45759
|
process.stderr.write(`[relay] shutdown reason=SIGTERM pid=${process.pid}
|
|
45714
45760
|
`);
|
|
45715
45761
|
try {
|
|
@@ -45722,6 +45768,7 @@ async function doBoot() {
|
|
|
45722
45768
|
process.once("SIGINT", async () => {
|
|
45723
45769
|
if (shuttingDown) return;
|
|
45724
45770
|
shuttingDown = true;
|
|
45771
|
+
eviction.stop();
|
|
45725
45772
|
process.stderr.write(`[relay] shutdown reason=SIGINT pid=${process.pid}
|
|
45726
45773
|
`);
|
|
45727
45774
|
try {
|
|
@@ -48129,7 +48176,8 @@ ${sections.join("\n\n")}` }] };
|
|
|
48129
48176
|
if (_utility_task_id) {
|
|
48130
48177
|
const stashedMeta = _pendingSkillData.get(_utility_task_id);
|
|
48131
48178
|
_pendingSkillData.delete(_utility_task_id);
|
|
48132
|
-
const utilityResult = ctx.nativeResultMap.get(_utility_task_id);
|
|
48179
|
+
const utilityResult = ctx.nativeUtilityResultMap.get(_utility_task_id) ?? ctx.nativeResultMap.get(_utility_task_id);
|
|
48180
|
+
ctx.nativeUtilityResultMap.delete(_utility_task_id);
|
|
48133
48181
|
ctx.nativeResultMap.delete(_utility_task_id);
|
|
48134
48182
|
ctx.nativeTaskMap.delete(_utility_task_id);
|
|
48135
48183
|
const _guardBefore = _utilityGuardSnapshots.get(_utility_task_id);
|
|
@@ -48195,6 +48243,10 @@ ${preview}` }]
|
|
|
48195
48243
|
timeoutMs: 12e4,
|
|
48196
48244
|
utilityType: "skill_develop"
|
|
48197
48245
|
});
|
|
48246
|
+
try {
|
|
48247
|
+
ctx.mainAgent.recordNativeTask(taskId, "_utility", `skill_develop:${category}`);
|
|
48248
|
+
} catch {
|
|
48249
|
+
}
|
|
48198
48250
|
spawnTimeoutWatcher(taskId, ctx.nativeTaskMap.get(taskId));
|
|
48199
48251
|
const modelShort = ctx.nativeUtilityConfig.model;
|
|
48200
48252
|
return {
|
package/docs/HANDBOOK.md
CHANGED
|
@@ -114,6 +114,25 @@ The `verify-the-premise` skill (premise-verification Stage 1) auto-binds to ever
|
|
|
114
114
|
- The change is documentation, CSS, test data adjustments, or log-string-only
|
|
115
115
|
- Under 10 lines, no side effects on shared state, no security surface
|
|
116
116
|
|
|
117
|
+
### When to gate merges on consensus — impact-adjacency, not just LOC
|
|
118
|
+
|
|
119
|
+
Pre-merge consensus is cheap; bugs in trust-boundary-adjacent code are expensive. Run `gossip_dispatch(mode: "consensus", ...)` on a PR **regardless of size** when the diff touches any of:
|
|
120
|
+
|
|
121
|
+
- **Shared in-memory state with lifecycle hooks** — any `Map`, `Set`, or cache where entries are added in one path and removed (or expire) in another. Race conditions and memory leaks hide here even in small diffs.
|
|
122
|
+
- **Background cleanup / TTL / timer logic** — scheduled eviction, session expiry, job-queue draining, or any timer whose firing sequence is observable externally. A missed edge case silently accumulates state.
|
|
123
|
+
- **Serialization at persistence boundaries** — code that writes or parses structured files (JSON config, YAML frontmatter, append-only logs, DB migrations). A format regression is invisible until a downstream reader breaks, and the blast radius is multiplicative.
|
|
124
|
+
- **Authentication / authorization boundaries** — middleware, token validation, permission checks, or any decorator that gates access. A single missing condition opens the full surface behind it.
|
|
125
|
+
- **Signal or event pipelines** — pub/sub routing, queue producers/consumers, or any path where message loss, duplication, or ordering is externally observable. Subtle ordering bugs are hard to reproduce after the fact.
|
|
126
|
+
- **Install / bootstrap / initialization paths** — postinstall hooks, config writers, seeding scripts, or startup sequences. These run once and their failures are often silent or hard to roll back.
|
|
127
|
+
|
|
128
|
+
**Why LOC alone misses these:** risk is proportional to how many concurrent systems depend on implicit assumptions, not to code size. A 40-line change to a cache eviction path or a config merge routine can introduce a memory leak or clobber user state just as easily as a 500-line feature. Let impact adjacency — not file count — trigger consensus.
|
|
129
|
+
|
|
130
|
+
**Exceptions within impact-adjacent areas:**
|
|
131
|
+
- Small mirror-of-existing-pattern changes (applying a previously-reviewed fix to a sibling file) don't need a fresh round; the pattern is already validated.
|
|
132
|
+
- Pure log-string or comment edits inside these files are fine.
|
|
133
|
+
|
|
134
|
+
**Remediation when missed:** run post-hoc consensus on the merged diff, file follow-up PRs if findings surface, and document the gap as feedback.
|
|
135
|
+
|
|
117
136
|
### Consensus protocol — 3 steps
|
|
118
137
|
|
|
119
138
|
When you dispatch with `mode: "consensus"`, the orchestrator follows **three** steps. Phase 2 cross-review runs server-side automatically.
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Never shipped in the package tarball — always regenerated per machine.
|
|
7
7
|
*/
|
|
8
8
|
const { join, resolve } = require('path');
|
|
9
|
-
const { existsSync, writeFileSync } = require('fs');
|
|
9
|
+
const { existsSync, readFileSync, writeFileSync, statSync } = require('fs');
|
|
10
10
|
|
|
11
11
|
const scriptDir = __dirname; // .../gossipcat/scripts/
|
|
12
12
|
const packageRoot = resolve(scriptDir, '..'); // .../gossipcat/
|
|
@@ -19,29 +19,95 @@ const mcpServerPath = join(packageRoot, 'dist-mcp', 'mcp-server.js');
|
|
|
19
19
|
const isGlobal = process.env.npm_config_global === 'true';
|
|
20
20
|
const isGitClone = existsSync(join(packageRoot, '.git'));
|
|
21
21
|
|
|
22
|
-
// For git clones: skip if .mcp.json already exists
|
|
22
|
+
// For git clones: skip writing if .mcp.json already exists. Still warn if the
|
|
23
|
+
// built server is older than package.json — stale-build warning is the most
|
|
24
|
+
// valuable signal on a developer re-running npm install after a pull.
|
|
23
25
|
if (isGitClone && existsSync(join(packageRoot, '.mcp.json'))) {
|
|
26
|
+
if (existsSync(mcpServerPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const serverMtime = statSync(mcpServerPath).mtime;
|
|
29
|
+
const pkgMtime = statSync(join(packageRoot, 'package.json')).mtime;
|
|
30
|
+
if (serverMtime < pkgMtime) {
|
|
31
|
+
console.log("gossipcat: dist-mcp/ is older than package.json — run 'npm run build:mcp' to rebuild");
|
|
32
|
+
}
|
|
33
|
+
} catch (_) { /* stat failure is non-fatal */ }
|
|
34
|
+
}
|
|
24
35
|
console.log('gossipcat: .mcp.json already exists — skipping (git clone)');
|
|
25
36
|
process.exit(0);
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
// For project-local installs: write .mcp.json into the consumer project root,
|
|
29
|
-
// not into node_modules/gossipcat.
|
|
30
|
-
//
|
|
40
|
+
// not into node_modules/gossipcat. Walk up from __dirname until a package.json
|
|
41
|
+
// with a "workspaces" field is found OR filesystem root is reached.
|
|
31
42
|
let outputDir = packageRoot; // default: package dir (global or git clone)
|
|
32
43
|
if (!isGlobal && !isGitClone) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
let found = false;
|
|
45
|
+
// Start the walk ONE level ABOVE packageRoot so we skip gossipcat's own
|
|
46
|
+
// package.json (which has a "workspaces" field for its own monorepo layout
|
|
47
|
+
// and would otherwise match on iteration 1, writing .mcp.json into
|
|
48
|
+
// node_modules/gossipcat/ instead of the consumer's project root).
|
|
49
|
+
let dir = resolve(packageRoot, '..');
|
|
50
|
+
while (true) {
|
|
51
|
+
const pkgPath = join(dir, 'package.json');
|
|
52
|
+
if (existsSync(pkgPath)) {
|
|
53
|
+
try {
|
|
54
|
+
const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf8'));
|
|
55
|
+
if (pkg.workspaces) {
|
|
56
|
+
outputDir = dir;
|
|
57
|
+
found = true;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
} catch (_) { /* unparseable package.json — keep walking */ }
|
|
61
|
+
}
|
|
62
|
+
const parent = resolve(dir, '..');
|
|
63
|
+
if (parent === dir) break; // filesystem root
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
if (!found) {
|
|
67
|
+
// Fallback: two-level walk-up from node_modules (original heuristic)
|
|
68
|
+
const nodeModulesDir = resolve(packageRoot, '..');
|
|
69
|
+
const projectRoot = resolve(nodeModulesDir, '..');
|
|
70
|
+
if (existsSync(join(projectRoot, 'package.json'))) {
|
|
71
|
+
outputDir = projectRoot;
|
|
72
|
+
} else {
|
|
73
|
+
outputDir = process.cwd();
|
|
74
|
+
}
|
|
38
75
|
}
|
|
39
76
|
}
|
|
40
77
|
|
|
41
78
|
const mcpConfig = join(outputDir, '.mcp.json');
|
|
42
79
|
|
|
80
|
+
// Preserve existing user-added MCP server entries. An existing .mcp.json may
|
|
81
|
+
// carry servers other than gossipcat (e.g. another MCP tool the user has
|
|
82
|
+
// installed). Unconditional overwrite would silently clobber those entries
|
|
83
|
+
// on every npm install. Merge strategy:
|
|
84
|
+
// - Parse existing file if present. On JSON error, skip the write (don't
|
|
85
|
+
// destroy what we can't parse; user can fix manually).
|
|
86
|
+
// - Preserve every top-level field via spread, preserve every existing
|
|
87
|
+
// mcpServers entry, and refresh (or insert) the gossipcat entry.
|
|
88
|
+
let existing = {};
|
|
89
|
+
if (existsSync(mcpConfig)) {
|
|
90
|
+
try {
|
|
91
|
+
const raw = readFileSync(mcpConfig, 'utf8');
|
|
92
|
+
const parsed = JSON.parse(raw);
|
|
93
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
94
|
+
existing = parsed;
|
|
95
|
+
}
|
|
96
|
+
} catch (_) {
|
|
97
|
+
console.warn(`gossipcat: existing .mcp.json at ${mcpConfig} is malformed — skipping update to avoid clobbering user config`);
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const existingServers =
|
|
103
|
+
existing.mcpServers && typeof existing.mcpServers === 'object' && !Array.isArray(existing.mcpServers)
|
|
104
|
+
? existing.mcpServers
|
|
105
|
+
: {};
|
|
106
|
+
|
|
43
107
|
const config = {
|
|
108
|
+
...existing,
|
|
44
109
|
mcpServers: {
|
|
110
|
+
...existingServers,
|
|
45
111
|
gossipcat: {
|
|
46
112
|
command: 'node',
|
|
47
113
|
args: [mcpServerPath],
|
|
@@ -54,14 +120,37 @@ try {
|
|
|
54
120
|
const method = isGlobal ? 'global npm' : isGitClone ? 'git clone' : 'local install';
|
|
55
121
|
console.log(`gossipcat: wrote .mcp.json (${method}) → ${mcpServerPath}`);
|
|
56
122
|
} catch (e) {
|
|
123
|
+
// Soft-fail on write errors: global npm installs often target root-owned dirs
|
|
124
|
+
// where EACCES is expected. Hard-exiting here would abort `npm install -g`
|
|
125
|
+
// for every user without sudo — users can re-run `gossipcat setup` after.
|
|
126
|
+
// Regression guard: tests/cli/install-packaging.test.ts:133-140.
|
|
57
127
|
console.warn(`gossipcat: postinstall could not write .mcp.json (${e.code || e.message}). Run 'gossipcat setup' after install to configure.`);
|
|
58
128
|
}
|
|
59
129
|
|
|
130
|
+
// Staleness check: warn if dist-mcp/ is older than package.json
|
|
131
|
+
if (existsSync(mcpServerPath)) {
|
|
132
|
+
try {
|
|
133
|
+
const serverMtime = statSync(mcpServerPath).mtime;
|
|
134
|
+
const pkgMtime = statSync(join(packageRoot, 'package.json')).mtime;
|
|
135
|
+
if (serverMtime < pkgMtime) {
|
|
136
|
+
console.log("gossipcat: dist-mcp/ is older than package.json — run 'npm run build:mcp' to rebuild");
|
|
137
|
+
}
|
|
138
|
+
} catch (_) { /* stat failure is non-fatal */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
60
141
|
if (!existsSync(mcpServerPath)) {
|
|
61
142
|
if (isGitClone) {
|
|
62
|
-
|
|
143
|
+
// Git clone: build is required — run it automatically
|
|
144
|
+
const { execSync } = require('child_process');
|
|
145
|
+
console.log('gossipcat: dist-mcp/mcp-server.js not built yet — running npm run build:mcp...');
|
|
146
|
+
try {
|
|
147
|
+
execSync('npm run build:mcp', { stdio: 'inherit', cwd: packageRoot });
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.error('gossipcat: build failed. Run "npm run build:mcp" manually to complete setup.');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
63
152
|
} else {
|
|
64
|
-
console.error('gossipcat: FATAL — dist-mcp/mcp-server.js missing from package. Install is corrupted
|
|
153
|
+
console.error('gossipcat: FATAL — dist-mcp/mcp-server.js missing from package. Install is corrupted; reinstall with `npm install -g gossipcat` or clone the repo and run `npm install && npm run build:mcp`.');
|
|
65
154
|
process.exit(1);
|
|
66
155
|
}
|
|
67
156
|
}
|