@rotifer/mcp-server 0.17.0 → 0.19.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/README.md +29 -0
- package/dist/channel.d.ts +39 -0
- package/dist/channel.js +72 -0
- package/dist/channel.js.map +1 -0
- package/dist/cloud.d.ts +33 -2
- package/dist/cloud.js +102 -10
- package/dist/cloud.js.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/local.d.ts +2 -0
- package/dist/local.js +27 -4
- package/dist/local.js.map +1 -1
- package/dist/self-update.d.ts +45 -0
- package/dist/self-update.js +209 -0
- package/dist/self-update.js.map +1 -0
- package/dist/server.js +30 -2
- package/dist/server.js.map +1 -1
- package/dist/telemetry/config.d.ts +40 -0
- package/dist/telemetry/config.js +128 -0
- package/dist/telemetry/config.js.map +1 -0
- package/dist/telemetry/consent.d.ts +28 -0
- package/dist/telemetry/consent.js +40 -0
- package/dist/telemetry/consent.js.map +1 -0
- package/dist/telemetry/heartbeat.d.ts +11 -0
- package/dist/telemetry/heartbeat.js +90 -0
- package/dist/telemetry/heartbeat.js.map +1 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +6 -3
- package/dist/version.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage and consent state for the anonymous usage heartbeat (ADR-329).
|
|
3
|
+
*
|
|
4
|
+
* Lives in ~/.rotifer/telemetry.json, shared in practice with
|
|
5
|
+
* rotifer-playground on the same machine: both processes read and write this
|
|
6
|
+
* file, so a machine_id minted by one is picked up by the other rather than
|
|
7
|
+
* each inventing its own. The on-disk shape (enabled / machine_id /
|
|
8
|
+
* consent_source / first_run_notice_shown / updated_at) is copied field-for-
|
|
9
|
+
* field from playground's src/telemetry/config.ts — keep the two in sync, or
|
|
10
|
+
* one process's write starts looking like corruption to the other's reader.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { telemetryOptedOutByEnv, telemetryExplicitlyOnByEnv } from "./consent.js";
|
|
16
|
+
const ROTIFER_HOME = join(process.env.HOME || process.env.USERPROFILE || "/tmp", ".rotifer");
|
|
17
|
+
const TELEMETRY_CONFIG_FILE = "telemetry.json";
|
|
18
|
+
// mcp-server has no equivalent of playground's utils/private-fs.ts — this is
|
|
19
|
+
// that module's two functions, copied rather than imported (again: separate
|
|
20
|
+
// npm packages). Kept deliberately this small; if a third file in this
|
|
21
|
+
// package ever needs the same thing, promote it to its own module then.
|
|
22
|
+
function ensurePrivateDir(dirPath) {
|
|
23
|
+
if (!existsSync(dirPath)) {
|
|
24
|
+
mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
chmodSync(dirPath, 0o700);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Best-effort hardening on existing directories.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function tightenPrivateFile(filePath, mode = 0o600) {
|
|
34
|
+
try {
|
|
35
|
+
chmodSync(filePath, mode);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Best-effort hardening on files that were just created or updated.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function telemetryConfigPath() {
|
|
42
|
+
return join(ROTIFER_HOME, TELEMETRY_CONFIG_FILE);
|
|
43
|
+
}
|
|
44
|
+
function readStoredConfig() {
|
|
45
|
+
const p = telemetryConfigPath();
|
|
46
|
+
if (!existsSync(p))
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(readFileSync(p, "utf-8"));
|
|
50
|
+
if (typeof parsed.machine_id !== "string" || !parsed.machine_id)
|
|
51
|
+
return null;
|
|
52
|
+
return {
|
|
53
|
+
enabled: parsed.enabled !== false,
|
|
54
|
+
machine_id: parsed.machine_id,
|
|
55
|
+
consent_source: parsed.consent_source ?? "default-notice",
|
|
56
|
+
first_run_notice_shown: parsed.first_run_notice_shown === true,
|
|
57
|
+
updated_at: parsed.updated_at ?? new Date(0).toISOString(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Corrupt file: treated as absent, a fresh machine_id is minted. Losing
|
|
62
|
+
// continuity once is an acceptable cost; refusing to run telemetry at
|
|
63
|
+
// all until a human fixes the file is not — this is opt-out telemetry.
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function writeStoredConfig(config) {
|
|
68
|
+
ensurePrivateDir(ROTIFER_HOME);
|
|
69
|
+
const p = telemetryConfigPath();
|
|
70
|
+
writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
71
|
+
tightenPrivateFile(p);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Reads the stored config, minting one (with a fresh machine_id) on first
|
|
75
|
+
* ever call. Cheap and idempotent — no reason to cache across calls within a
|
|
76
|
+
* process; a concurrent `rotifer telemetry off` run from the CLI on the same
|
|
77
|
+
* machine should be picked up by the next tool call, not shadowed by a stale
|
|
78
|
+
* in-memory copy held by a long-lived MCP server process.
|
|
79
|
+
*/
|
|
80
|
+
export function loadOrInitHeartbeatConfig() {
|
|
81
|
+
const existing = readStoredConfig();
|
|
82
|
+
if (existing)
|
|
83
|
+
return existing;
|
|
84
|
+
const fresh = {
|
|
85
|
+
enabled: true,
|
|
86
|
+
machine_id: randomUUID(),
|
|
87
|
+
consent_source: "default-notice",
|
|
88
|
+
first_run_notice_shown: false,
|
|
89
|
+
updated_at: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
writeStoredConfig(fresh);
|
|
92
|
+
return fresh;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Persists that the notice has now been shown — separated from printing/
|
|
96
|
+
* returning it (heartbeat.ts) so the persistence, which must happen exactly
|
|
97
|
+
* once and survive even if the network call that follows fails, is not
|
|
98
|
+
* tangled up with how the notice reaches the user.
|
|
99
|
+
*/
|
|
100
|
+
export function markFirstRunNoticeShown(config) {
|
|
101
|
+
if (config.first_run_notice_shown)
|
|
102
|
+
return;
|
|
103
|
+
writeStoredConfig({ ...config, first_run_notice_shown: true, updated_at: new Date().toISOString() });
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolution order, mirrored in TELEMETRY.md and kept in sync deliberately:
|
|
107
|
+
* DO_NOT_TRACK / ROTIFER_TELEMETRY (env, either direction) > stored choice
|
|
108
|
+
* > default on.
|
|
109
|
+
*
|
|
110
|
+
* The heartbeat's default is the opposite of the signed-in invocation
|
|
111
|
+
* report's (default-off) on purpose — see migration 20260830010000's header
|
|
112
|
+
* in rotifer-playground: this table carries no per-row accountability to
|
|
113
|
+
* protect, and default-off here would just reproduce the blind spot
|
|
114
|
+
* ADR-329 exists to close.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveHeartbeatDecision(config, env = process.env) {
|
|
117
|
+
if (telemetryOptedOutByEnv(env))
|
|
118
|
+
return "off-env";
|
|
119
|
+
if (telemetryExplicitlyOnByEnv(env))
|
|
120
|
+
return "on-env";
|
|
121
|
+
if (!config.enabled)
|
|
122
|
+
return "off-stored";
|
|
123
|
+
return config.consent_source === "default-notice" ? "on-default" : "on-stored";
|
|
124
|
+
}
|
|
125
|
+
export function heartbeatDecisionEnabled(d) {
|
|
126
|
+
return d === "on-env" || d === "on-stored" || d === "on-default";
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/telemetry/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one environment-variable check both telemetry paths share.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors rotifer-playground's src/telemetry/consent.ts exactly — same
|
|
5
|
+
* logic, independently implemented because these are two separate npm
|
|
6
|
+
* packages that cannot import each other's source. If this file and that one
|
|
7
|
+
* ever diverge, fix both; a user who sets ROTIFER_TELEMETRY=0 expects it to
|
|
8
|
+
* mean "off" from whichever binary they happen to be running.
|
|
9
|
+
*
|
|
10
|
+
* ADR-329's decision is explicit: "ROTIFER_TELEMETRY=0 同时关闭匿名信号与
|
|
11
|
+
* 登录上报——「关掉」意味着全部,与 316 的语义一致." Before this module
|
|
12
|
+
* existed, ROTIFER_TELEMETRY was checked in exactly one place here
|
|
13
|
+
* (cloud.ts's telemetryOptedOut, for the signed-in invocation/download
|
|
14
|
+
* reports). Adding the anonymous heartbeat with its own copy of that check
|
|
15
|
+
* would have been the easy path and the wrong one.
|
|
16
|
+
*
|
|
17
|
+
* DO_NOT_TRACK is the cross-tool convention (https://consoledonottrack.com/)
|
|
18
|
+
* and is new here — it outranks ROTIFER_TELEMETRY because it is a stance the
|
|
19
|
+
* user took before ever hearing of this project.
|
|
20
|
+
*/
|
|
21
|
+
export declare function telemetryOptedOutByEnv(env?: NodeJS.ProcessEnv): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* True when the environment explicitly asked for telemetry (as opposed to
|
|
24
|
+
* "unset, so whatever the default is"). An explicit ROTIFER_TELEMETRY=1
|
|
25
|
+
* should win even over a stored "disabled" choice — a per-shell override
|
|
26
|
+
* should not require first running a separate command to change stored state.
|
|
27
|
+
*/
|
|
28
|
+
export declare function telemetryExplicitlyOnByEnv(env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one environment-variable check both telemetry paths share.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors rotifer-playground's src/telemetry/consent.ts exactly — same
|
|
5
|
+
* logic, independently implemented because these are two separate npm
|
|
6
|
+
* packages that cannot import each other's source. If this file and that one
|
|
7
|
+
* ever diverge, fix both; a user who sets ROTIFER_TELEMETRY=0 expects it to
|
|
8
|
+
* mean "off" from whichever binary they happen to be running.
|
|
9
|
+
*
|
|
10
|
+
* ADR-329's decision is explicit: "ROTIFER_TELEMETRY=0 同时关闭匿名信号与
|
|
11
|
+
* 登录上报——「关掉」意味着全部,与 316 的语义一致." Before this module
|
|
12
|
+
* existed, ROTIFER_TELEMETRY was checked in exactly one place here
|
|
13
|
+
* (cloud.ts's telemetryOptedOut, for the signed-in invocation/download
|
|
14
|
+
* reports). Adding the anonymous heartbeat with its own copy of that check
|
|
15
|
+
* would have been the easy path and the wrong one.
|
|
16
|
+
*
|
|
17
|
+
* DO_NOT_TRACK is the cross-tool convention (https://consoledonottrack.com/)
|
|
18
|
+
* and is new here — it outranks ROTIFER_TELEMETRY because it is a stance the
|
|
19
|
+
* user took before ever hearing of this project.
|
|
20
|
+
*/
|
|
21
|
+
export function telemetryOptedOutByEnv(env = process.env) {
|
|
22
|
+
const dnt = (env.DO_NOT_TRACK || "").trim().toLowerCase();
|
|
23
|
+
if (dnt !== "" && dnt !== "0" && dnt !== "false")
|
|
24
|
+
return true;
|
|
25
|
+
const flag = (env.ROTIFER_TELEMETRY || "").trim().toLowerCase();
|
|
26
|
+
return flag === "0" || flag === "false" || flag === "off";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* True when the environment explicitly asked for telemetry (as opposed to
|
|
30
|
+
* "unset, so whatever the default is"). An explicit ROTIFER_TELEMETRY=1
|
|
31
|
+
* should win even over a stored "disabled" choice — a per-shell override
|
|
32
|
+
* should not require first running a separate command to change stored state.
|
|
33
|
+
*/
|
|
34
|
+
export function telemetryExplicitlyOnByEnv(env = process.env) {
|
|
35
|
+
if (telemetryOptedOutByEnv(env))
|
|
36
|
+
return false;
|
|
37
|
+
const flag = (env.ROTIFER_TELEMETRY || "").trim().toLowerCase();
|
|
38
|
+
return flag !== "" && flag !== "0" && flag !== "false" && flag !== "off";
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=consent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent.js","sourceRoot":"","sources":["../../src/telemetry/consent.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACzE,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC1D,IAAI,GAAG,KAAK,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IAE9D,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAChE,OAAO,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,KAAK,CAAC;AAC5D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7E,IAAI,sBAAsB,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9C,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAChE,OAAO,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,KAAK,CAAC;AAC3E,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports one Gene invocation's worth of activity for this machine, today,
|
|
3
|
+
* on the given channel. Fire-and-forget; every failure mode is silence.
|
|
4
|
+
*
|
|
5
|
+
* Returns the first-run notice text exactly once — the first time a report
|
|
6
|
+
* for this machine is actually about to leave the process, never before (a
|
|
7
|
+
* machine whose heartbeat is off, by env or stored choice, must never be
|
|
8
|
+
* told "we're about to collect data" for data that in fact never leaves it)
|
|
9
|
+
* — and null every other time, including every call after the first.
|
|
10
|
+
*/
|
|
11
|
+
export declare function recordHeartbeat(channel: string): string | null;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anonymous usage heartbeat (ADR-329).
|
|
3
|
+
*
|
|
4
|
+
* Answers a narrower question than the signed-in invocation report: not "who
|
|
5
|
+
* called what", just "did this machine run something today, and through
|
|
6
|
+
* which channel". No identity, on by default, and it costs nothing to lose
|
|
7
|
+
* one — this is not §33.4 input, so unlike logGeneInvocation's report this
|
|
8
|
+
* file tracks no in-flight requests for anything to await. A dropped
|
|
9
|
+
* heartbeat is not a hole in an audited ledger, it is one machine's "active
|
|
10
|
+
* today" signal that will show up again tomorrow if the machine is still in
|
|
11
|
+
* use.
|
|
12
|
+
*
|
|
13
|
+
* The one thing genuinely different from playground's version of this file:
|
|
14
|
+
* the first-run notice cannot go to stderr here. stderr is where CLI users
|
|
15
|
+
* look and exactly where MCP hosts do not — nobody reads a long-lived MCP
|
|
16
|
+
* server's stderr stream in normal use. So recordHeartbeat() *returns* the
|
|
17
|
+
* notice text instead of printing it, and the caller (server.ts) folds it
|
|
18
|
+
* into the tool response the user's client will actually render.
|
|
19
|
+
*/
|
|
20
|
+
import { loadCloudConfig } from "../cloud.js";
|
|
21
|
+
import { getPackageVersion } from "../version.js";
|
|
22
|
+
import { loadOrInitHeartbeatConfig, resolveHeartbeatDecision, heartbeatDecisionEnabled, markFirstRunNoticeShown, } from "./config.js";
|
|
23
|
+
const FIRST_RUN_NOTICE = "Anonymous usage heartbeat is on by default — no code, no identity, just " +
|
|
24
|
+
'"this machine ran something today". Set ROTIFER_TELEMETRY=0 or DO_NOT_TRACK=1 ' +
|
|
25
|
+
"to disable. Details: https://rotifer.dev/telemetry";
|
|
26
|
+
/**
|
|
27
|
+
* Reports one Gene invocation's worth of activity for this machine, today,
|
|
28
|
+
* on the given channel. Fire-and-forget; every failure mode is silence.
|
|
29
|
+
*
|
|
30
|
+
* Returns the first-run notice text exactly once — the first time a report
|
|
31
|
+
* for this machine is actually about to leave the process, never before (a
|
|
32
|
+
* machine whose heartbeat is off, by env or stored choice, must never be
|
|
33
|
+
* told "we're about to collect data" for data that in fact never leaves it)
|
|
34
|
+
* — and null every other time, including every call after the first.
|
|
35
|
+
*/
|
|
36
|
+
export function recordHeartbeat(channel) {
|
|
37
|
+
// The whole body is one try/catch, deliberately wider than the network
|
|
38
|
+
// call — loadOrInitHeartbeatConfig()/markFirstRunNoticeShown() write to
|
|
39
|
+
// ~/.rotifer/telemetry.json, and a tool call failing because this
|
|
40
|
+
// fire-and-forget signal couldn't touch the filesystem would be a much
|
|
41
|
+
// worse trade than just not sending a heartbeat this once.
|
|
42
|
+
try {
|
|
43
|
+
const config = loadOrInitHeartbeatConfig();
|
|
44
|
+
const decision = resolveHeartbeatDecision(config);
|
|
45
|
+
if (!heartbeatDecisionEnabled(decision))
|
|
46
|
+
return null;
|
|
47
|
+
// Persisted synchronously, before the fetch below, so a crash mid-request
|
|
48
|
+
// can never cause the notice to be owed again on the next call.
|
|
49
|
+
let notice = null;
|
|
50
|
+
if (!config.first_run_notice_shown) {
|
|
51
|
+
notice = FIRST_RUN_NOTICE;
|
|
52
|
+
markFirstRunNoticeShown(config);
|
|
53
|
+
}
|
|
54
|
+
const cloudConfig = loadCloudConfig();
|
|
55
|
+
const url = `${cloudConfig.endpoint.replace(/\/+$/, "")}/rest/v1/rpc/record_heartbeat`;
|
|
56
|
+
// No Authorization header: record_heartbeat is anon-callable by design
|
|
57
|
+
// (playground migration 20260830010000) — that grant is the entire point
|
|
58
|
+
// of ADR-329. Sending credentials here would be pointless (the RPC does
|
|
59
|
+
// not use them) and would defeat the "no identity" promise for a caller
|
|
60
|
+
// who happens to be signed in but hasn't opted into the invocation report.
|
|
61
|
+
fetch(url, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/json", apikey: cloudConfig.anonKey },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
p_machine_id: config.machine_id,
|
|
66
|
+
p_channel: channel,
|
|
67
|
+
p_client_version: getPackageVersion(),
|
|
68
|
+
p_invocation_delta: 1,
|
|
69
|
+
}),
|
|
70
|
+
})
|
|
71
|
+
.then((res) => {
|
|
72
|
+
if (!res.ok && process.env.ROTIFER_DEBUG) {
|
|
73
|
+
process.stderr.write(`[rotifer] record_heartbeat failed (${res.status})\n`);
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.catch((err) => {
|
|
77
|
+
if (process.env.ROTIFER_DEBUG) {
|
|
78
|
+
process.stderr.write(`[rotifer] record_heartbeat error: ${err?.message ?? err}\n`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return notice;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (process.env.ROTIFER_DEBUG) {
|
|
85
|
+
process.stderr.write(`[rotifer] record_heartbeat setup error: ${err?.message ?? err}\n`);
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=heartbeat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../../src/telemetry/heartbeat.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,wBAAwB,EACxB,uBAAuB,GACxB,MAAM,aAAa,CAAC;AAErB,MAAM,gBAAgB,GACpB,0EAA0E;IAC1E,gFAAgF;IAChF,oDAAoD,CAAC;AAEvD;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,uEAAuE;IACvE,wEAAwE;IACxE,kEAAkE;IAClE,uEAAuE;IACvE,2DAA2D;IAC3D,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,yBAAyB,EAAE,CAAC;QAC3C,MAAM,QAAQ,GAAG,wBAAwB,CAAC,MAAM,CAAC,CAAC;QAClD,IAAI,CAAC,wBAAwB,CAAC,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;QAErD,0EAA0E;QAC1E,gEAAgE;QAChE,IAAI,MAAM,GAAkB,IAAI,CAAC;QACjC,IAAI,CAAC,MAAM,CAAC,sBAAsB,EAAE,CAAC;YACnC,MAAM,GAAG,gBAAgB,CAAC;YAC1B,uBAAuB,CAAC,MAAM,CAAC,CAAC;QAClC,CAAC;QAED,MAAM,WAAW,GAAG,eAAe,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,GAAG,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,+BAA+B,CAAC;QAEvF,uEAAuE;QACvE,yEAAyE;QACzE,wEAAwE;QACxE,wEAAwE;QACxE,2EAA2E;QAC3E,KAAK,CAAC,GAAG,EAAE;YACT,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,EAAE;YAC5E,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,YAAY,EAAE,MAAM,CAAC,UAAU;gBAC/B,SAAS,EAAE,OAAO;gBAClB,gBAAgB,EAAE,iBAAiB,EAAE;gBACrC,kBAAkB,EAAE,CAAC;aACtB,CAAC;SACH,CAAC;aACC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;YACZ,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;gBACzC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sCAAsC,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC;YAC9E,CAAC;QACH,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;YACtB,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;gBAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAsC,GAAa,EAAE,OAAO,IAAI,GAAG,IAAI,CAAC,CAAC;YAChG,CAAC;QACH,CAAC,CAAC,CAAC;QAEL,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACtB,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC;YAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2CAA4C,GAAa,EAAE,OAAO,IAAI,GAAG,IAAI,CAAC,CAAC;QACtG,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
export declare function getConfigDir(): string;
|
|
1
2
|
export declare function getPackageVersion(): string;
|
|
3
|
+
export declare function compareSemver(a: string, b: string): number;
|
|
4
|
+
/** A live registry lookup, bypassing the 24h cache in getVersionInfo().
|
|
5
|
+
* `self-update` needs the truth at the moment the user asked, not a cached
|
|
6
|
+
* answer from yesterday. */
|
|
7
|
+
export declare function fetchLatestVersion(): Promise<string | null>;
|
|
2
8
|
export interface VersionInfo {
|
|
3
9
|
current: string;
|
|
4
10
|
latest: string | null;
|
package/dist/version.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
6
6
|
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
7
|
-
function getConfigDir() {
|
|
7
|
+
export function getConfigDir() {
|
|
8
8
|
return process.env.ROTIFER_CONFIG_DIR || join(homedir(), ".config", "rotifer");
|
|
9
9
|
}
|
|
10
10
|
function getCacheFile() {
|
|
@@ -17,7 +17,7 @@ export function getPackageVersion() {
|
|
|
17
17
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
18
18
|
return pkg.version;
|
|
19
19
|
}
|
|
20
|
-
function compareSemver(a, b) {
|
|
20
|
+
export function compareSemver(a, b) {
|
|
21
21
|
const pa = a.split(".").map(Number);
|
|
22
22
|
const pb = b.split(".").map(Number);
|
|
23
23
|
for (let i = 0; i < 3; i++) {
|
|
@@ -45,7 +45,10 @@ function writeCache(cache) {
|
|
|
45
45
|
}
|
|
46
46
|
catch { /* non-critical */ }
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
/** A live registry lookup, bypassing the 24h cache in getVersionInfo().
|
|
49
|
+
* `self-update` needs the truth at the moment the user asked, not a cached
|
|
50
|
+
* answer from yesterday. */
|
|
51
|
+
export async function fetchLatestVersion() {
|
|
49
52
|
try {
|
|
50
53
|
const controller = new AbortController();
|
|
51
54
|
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/D,MAAM,iBAAiB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE9C,
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rotifer/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"mcpName": "io.github.rotifer-protocol/mcp-server",
|
|
5
5
|
"description": "Rotifer MCP Server — AI agents search, compare, and rank Genes (modular AI skills) from a living ecosystem of 50+ capabilities. Zero config.",
|
|
6
6
|
"type": "module",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
},
|
|
53
53
|
"overrides": {
|
|
54
54
|
"qs": "^6.15.2",
|
|
55
|
-
"hono": "^4.12.26"
|
|
55
|
+
"hono": "^4.12.26",
|
|
56
|
+
"fast-uri": "^3.1.7"
|
|
56
57
|
},
|
|
57
58
|
"engines": {
|
|
58
59
|
"node": ">=20"
|