trantor 0.17.26 → 0.17.28
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/.claude-plugin/plugin.json +1 -1
- package/hooks/lib/update-check.mjs +104 -0
- package/hooks/sessionstart.mjs +36 -6
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.28",
|
|
4
4
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
|
|
5
5
|
"mcpServers": {
|
|
6
6
|
"relay": {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// trantor update-check — surfaces "a newer Trantor is available" the way desktop software does:
|
|
2
|
+
// a one-time desktop notification (macOS osascript / Linux notify-send) plus an in-session context
|
|
3
|
+
// block so the running model also tells the user the exact update commands.
|
|
4
|
+
//
|
|
5
|
+
// Design constraints (same contract as the other hooks): cheap, fail-silent, never blocks a session.
|
|
6
|
+
// • The installed version is self-discovered from the hook's OWN plugin.json (the plugin is installed at
|
|
7
|
+
// …/cache/trantor/trantor/<version>/…), so there's no guessing.
|
|
8
|
+
// • "latest" comes from the npm dist-tags endpoint — tiny + no auth — and is THROTTLED behind a TTL
|
|
9
|
+
// (default 6h) cached in ~/.agent-bus/update-check.json, so the vast majority of session starts do
|
|
10
|
+
// ZERO network. The fetch itself has a 1.5s timeout and any failure falls back to the cached value.
|
|
11
|
+
// • The desktop notification fires at most ONCE PER NEW VERSION (tracked by notifiedVersion), so it's
|
|
12
|
+
// not per-session spam — exactly one ping when a release lands, like a real updater.
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
14
|
+
import { join, dirname } from "node:path";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { execSync } from "node:child_process";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const HERE = dirname(fileURLToPath(import.meta.url)); // <version>/hooks/lib
|
|
20
|
+
const DATA = process.env.RELAY_DATA_DIR || join(homedir(), ".agent-bus");
|
|
21
|
+
const STAMP = join(DATA, "update-check.json");
|
|
22
|
+
const DIST_TAGS_URL = "https://registry.npmjs.org/-/package/trantor/dist-tags";
|
|
23
|
+
|
|
24
|
+
export function readConfig() {
|
|
25
|
+
try { const p = join(DATA, "config.json"); return existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : {}; }
|
|
26
|
+
catch { return {}; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function nowSec() { try { return Number(execSync("date +%s", { encoding: "utf8" }).trim()) || 0; } catch { return Math.floor(Date.now() / 1000); } }
|
|
30
|
+
function readStamp() { try { return JSON.parse(readFileSync(STAMP, "utf8")); } catch { return {}; } }
|
|
31
|
+
function writeStamp(o) { try { if (!existsSync(DATA)) mkdirSync(DATA, { recursive: true }); writeFileSync(STAMP, JSON.stringify(o, null, 2)); } catch {} }
|
|
32
|
+
|
|
33
|
+
// The version of the trantor plugin THIS hook is part of (…/hooks/lib → ../../.claude-plugin/plugin.json).
|
|
34
|
+
// Falls back to the package.json (covers running straight from the repo, where both sit at the root).
|
|
35
|
+
export function installedVersion() {
|
|
36
|
+
for (const rel of ["../../.claude-plugin/plugin.json", "../../package.json"]) {
|
|
37
|
+
try { const p = join(HERE, rel); if (existsSync(p)) { const v = JSON.parse(readFileSync(p, "utf8")).version; if (v) return v; } } catch {}
|
|
38
|
+
}
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Numeric a.b.c compare → -1 | 0 | 1. (Pre-release tags aren't used by trantor's release flow, so a
|
|
43
|
+
// plain numeric compare is correct and keeps this dependency-free.)
|
|
44
|
+
export function cmpSemver(a, b) {
|
|
45
|
+
const pa = String(a).split(".").map(n => parseInt(n, 10) || 0);
|
|
46
|
+
const pb = String(b).split(".").map(n => parseInt(n, 10) || 0);
|
|
47
|
+
for (let i = 0; i < 3; i++) { if ((pa[i] || 0) < (pb[i] || 0)) return -1; if ((pa[i] || 0) > (pb[i] || 0)) return 1; }
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Latest published version, throttled by TTL. Returns the cached value on a fresh-enough check or on any
|
|
52
|
+
// network failure; refetches (and re-stamps) only when the cache is stale. Never throws.
|
|
53
|
+
export async function latestVersion(conf = readConfig()) {
|
|
54
|
+
const ttlH = Number(process.env.TRANTOR_UPDATE_TTL_H || conf.updateCheckTtlHours || 6);
|
|
55
|
+
const stamp = readStamp();
|
|
56
|
+
if (stamp.latest && stamp.checkedAt && (nowSec() - stamp.checkedAt) < ttlH * 3600) return stamp.latest;
|
|
57
|
+
try {
|
|
58
|
+
const r = await fetch(DIST_TAGS_URL, { signal: AbortSignal.timeout(1500) });
|
|
59
|
+
const j = await r.json();
|
|
60
|
+
const latest = j?.latest || stamp.latest || "";
|
|
61
|
+
writeStamp({ ...stamp, checkedAt: nowSec(), latest });
|
|
62
|
+
return latest;
|
|
63
|
+
} catch { return stamp.latest || ""; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// { available, installed, latest } — `available` true only when installed < latest. Disabled by
|
|
67
|
+
// TRANTOR_NO_UPDATE_CHECK=1 or config.updateCheck:false.
|
|
68
|
+
export async function updateAvailable(conf = readConfig()) {
|
|
69
|
+
if (process.env.TRANTOR_NO_UPDATE_CHECK === "1" || conf.updateCheck === false) return { available: false, installed: "", latest: "" };
|
|
70
|
+
const installed = installedVersion();
|
|
71
|
+
const latest = await latestVersion(conf);
|
|
72
|
+
if (!installed || !latest) return { available: false, installed, latest };
|
|
73
|
+
return { available: cmpSemver(installed, latest) < 0, installed, latest };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Fire a native desktop notification — but only ONCE per new version (so multiple session starts don't
|
|
77
|
+
// each pop one). Returns true if it actually notified. Disabled by TRANTOR_NO_UPDATE_NOTIFY=1 or
|
|
78
|
+
// config.updateDesktopNotify:false. Best-effort; never throws.
|
|
79
|
+
export function maybeNotifyDesktop({ installed, latest } = {}, conf = readConfig()) {
|
|
80
|
+
try {
|
|
81
|
+
if (!latest) return false;
|
|
82
|
+
if (process.env.TRANTOR_NO_UPDATE_NOTIFY === "1" || conf.updateDesktopNotify === false) return false;
|
|
83
|
+
if (readStamp().notifiedVersion === latest) return false; // already told them about THIS version
|
|
84
|
+
const title = "Trantor update available";
|
|
85
|
+
const msg = `${installed || "?"} → ${latest}. Update: claude plugin update trantor@trantor`;
|
|
86
|
+
if (process.platform === "darwin") {
|
|
87
|
+
let done = false;
|
|
88
|
+
try { execSync("command -v terminal-notifier", { stdio: "ignore" });
|
|
89
|
+
execSync(`terminal-notifier -title ${JSON.stringify(title)} -message ${JSON.stringify(msg)} -group trantor-update`, { timeout: 3000 });
|
|
90
|
+
done = true;
|
|
91
|
+
} catch {}
|
|
92
|
+
if (!done) {
|
|
93
|
+
const osa = `display notification ${JSON.stringify(msg)} with title ${JSON.stringify(title)}`;
|
|
94
|
+
execSync(`osascript -e ${JSON.stringify(osa)}`, { timeout: 3000 });
|
|
95
|
+
}
|
|
96
|
+
} else if (process.platform === "linux") {
|
|
97
|
+
execSync(`notify-send ${JSON.stringify(title)} ${JSON.stringify(msg)}`, { timeout: 3000 });
|
|
98
|
+
} else {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
writeStamp({ ...readStamp(), notifiedVersion: latest });
|
|
102
|
+
return true;
|
|
103
|
+
} catch { return false; }
|
|
104
|
+
}
|
package/hooks/sessionstart.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { join, basename } from "node:path";
|
|
|
11
11
|
import { homedir, hostname } from "node:os";
|
|
12
12
|
import { execSync } from "node:child_process";
|
|
13
13
|
import { resolveProject, hostId } from "../lib/project.mjs";
|
|
14
|
+
import { updateAvailable, maybeNotifyDesktop, readConfig } from "./lib/update-check.mjs";
|
|
14
15
|
|
|
15
16
|
// Load the most recent UNCONSUMED handoff for this project (written by precompact.mjs
|
|
16
17
|
// / the heartbeat early-warning). `claim` marks it consumed so exactly one session
|
|
@@ -92,6 +93,7 @@ function sanitize(s) {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
let additionalContext = "";
|
|
96
|
+
let userBanner = ""; // shown to the USER in-terminal via the hook's `systemMessage` (not model-only context)
|
|
95
97
|
try {
|
|
96
98
|
let source = "", stdinObj = {};
|
|
97
99
|
try { stdinObj = JSON.parse((await readStdin()) || "{}"); source = stdinObj.source || ""; } catch {}
|
|
@@ -157,6 +159,26 @@ try {
|
|
|
157
159
|
}
|
|
158
160
|
} catch {}
|
|
159
161
|
|
|
162
|
+
// Update available? Surface it the way a terminal tool should — an in-terminal `systemMessage`
|
|
163
|
+
// line the USER sees at session start (NOT a macOS desktop popup, which macOS misattributes to
|
|
164
|
+
// Script Editor and which fires off-screen). It shows every session while an update is pending and
|
|
165
|
+
// auto-clears the moment they update (updateAvailable() flips false) — a persistent-until-resolved
|
|
166
|
+
// reminder, like the built-in MCP-disconnected indicator. The model also gets the <trantor-update>
|
|
167
|
+
// context block so it can give the exact commands on request. Desktop notification is now OPT-IN
|
|
168
|
+
// (config.updateDesktopNotify:true) for anyone who genuinely wants the OS-level ping.
|
|
169
|
+
// Throttled + fail-silent; most starts do zero network (6h TTL cache). Disable: TRANTOR_NO_UPDATE_CHECK.
|
|
170
|
+
try {
|
|
171
|
+
const upd = await updateAvailable();
|
|
172
|
+
if (upd.available) {
|
|
173
|
+
if (readConfig().updateDesktopNotify === true) maybeNotifyDesktop(upd); // opt-in only
|
|
174
|
+
userBanner = `⬆️ Trantor update available: ${upd.installed} → ${upd.latest} · update with: claude plugin update trantor@trantor`;
|
|
175
|
+
additionalContext += `<trantor-update installed="${sanitize(upd.installed)}" latest="${sanitize(upd.latest)}">\n`;
|
|
176
|
+
additionalContext += `⬆️ **A newer Trantor is available — ${sanitize(upd.installed)} → ${sanitize(upd.latest)}.** Tell the user, and offer the update: \`claude plugin update trantor@trantor\` (plugin) + \`npm i -g trantor@${sanitize(upd.latest)}\` (CLI), then restart to apply.\n`;
|
|
177
|
+
additionalContext += `</trantor-update>\n`;
|
|
178
|
+
process.stderr.write(`[trantor] update available: ${upd.installed} -> ${upd.latest}\n`);
|
|
179
|
+
}
|
|
180
|
+
} catch {}
|
|
181
|
+
|
|
160
182
|
// Pending handoff? A prior session hit the context limit and left a handoff for this
|
|
161
183
|
// project — take over with this fresh full window instead of starting cold. On a
|
|
162
184
|
// compaction-triggered start, DON'T claim it (that's the same session that wrote it;
|
|
@@ -179,13 +201,21 @@ try {
|
|
|
179
201
|
process.stderr.write(`[trantor] sessionstart error: ${err?.message || err}\n`);
|
|
180
202
|
}
|
|
181
203
|
|
|
182
|
-
// Hook protocol: emit additionalContext via stdout JSON
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
204
|
+
// Hook protocol: emit additionalContext (model-facing) via stdout JSON, plus an optional
|
|
205
|
+
// `systemMessage` (USER-facing — rendered as a line in the terminal, our update indicator).
|
|
206
|
+
// Self-validate so we never emit something Claude Code can't parse — fall back to sanitized, then {}.
|
|
207
|
+
function emit(ctx, sysMsg) {
|
|
208
|
+
const obj = {};
|
|
209
|
+
if (ctx) obj.hookSpecificOutput = { hookEventName: "SessionStart", additionalContext: ctx };
|
|
210
|
+
if (sysMsg) obj.systemMessage = sysMsg;
|
|
186
211
|
const out = JSON.stringify(obj);
|
|
187
212
|
try { JSON.parse(out); return out; } catch { /* fall through */ }
|
|
188
|
-
try {
|
|
213
|
+
try {
|
|
214
|
+
const safe = {};
|
|
215
|
+
if (ctx) safe.hookSpecificOutput = { hookEventName: "SessionStart", additionalContext: sanitize(ctx) };
|
|
216
|
+
if (sysMsg) safe.systemMessage = sanitize(sysMsg);
|
|
217
|
+
return JSON.stringify(safe);
|
|
218
|
+
} catch { return "{}"; }
|
|
189
219
|
}
|
|
190
|
-
process.stdout.write(emit(additionalContext));
|
|
220
|
+
process.stdout.write(emit(additionalContext, userBanner));
|
|
191
221
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"trantor": "bin/cli.mjs"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"zod": "^4.4.3"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-handoff.mjs"
|
|
13
|
+
"test": "node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-handoff.mjs && node test-update.mjs"
|
|
14
14
|
},
|
|
15
15
|
"description": "The hub-world for AI agent crews \u2014 orchestrate Claude Code, Codex, Gemini, Kimi & DeepSeek as live crews with a plan-aware Advisor, a Kanban/flow command center, a testing gate, and an economics brain (Scrooge).",
|
|
16
16
|
"files": [
|