trantor 0.17.8 → 0.17.9
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/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/hooks.json +3 -2
- package/hooks/todo-sync.mjs +52 -0
- package/hub.mjs +38 -0
- package/package.json +1 -1
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + context-handoff for independent AI coding agents (Claude, Codex, Gemini, …)",
|
|
9
|
-
"version": "0.17.
|
|
9
|
+
"version": "0.17.9"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "trantor",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "The hub-world for AI agent crews. Say \"fire up the crew\" and Claude becomes the architect: a plan-aware Advisor routes the work (solo / cheap inline calls / live crew of Codex, Gemini, Kimi & DeepSeek in their own terminal windows), a Kanban/flow command center with a testing gate tracks it, and an economics brain (Scrooge) keeps the receipts. Includes the relay MCP, a SessionStart auto-discovery hook, and a PreCompact context-handoff so a fresh session can take over a full window instead of compacting.",
|
|
16
|
-
"version": "0.17.
|
|
16
|
+
"version": "0.17.9",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Sasha Bogojevic"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.9",
|
|
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": {
|
package/hooks/hooks.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "trantor — auto-register each session + inject live roster (SessionStart); heartbeat presence on every tool call
|
|
2
|
+
"description": "trantor — auto-register each session + inject live roster (SessionStart); heartbeat presence on every tool call + mirror the session's TodoWrite list onto the board as cards (PostToolUse); write a handoff before compaction (PreCompact)",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{ "matcher": "", "hooks": [ { "type": "command", "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/sessionstart.mjs" } ] }
|
|
6
6
|
],
|
|
7
7
|
"PostToolUse": [
|
|
8
|
-
{ "matcher": "", "hooks": [ { "type": "command", "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/heartbeat.mjs" } ] }
|
|
8
|
+
{ "matcher": "", "hooks": [ { "type": "command", "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/heartbeat.mjs" } ] },
|
|
9
|
+
{ "matcher": "TodoWrite", "hooks": [ { "type": "command", "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/todo-sync.mjs" } ] }
|
|
9
10
|
],
|
|
10
11
|
"PreCompact": [
|
|
11
12
|
{ "matcher": "", "hooks": [ { "type": "command", "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/precompact.mjs" } ] }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// trantor PostToolUse(TodoWrite) — mirror the session's todo list onto its project board as cards,
|
|
3
|
+
// so SOLO work (no crew fired up) shows up live and accrues timeline history. The hub reconciles by
|
|
4
|
+
// todo text (pending/in_progress/completed -> todo/doing/done). Fail-silent by contract: a bad hub,
|
|
5
|
+
// a home-dir session, or any error must never block or break the tool flow.
|
|
6
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { join, basename } from "node:path";
|
|
8
|
+
import { homedir, hostname } from "node:os";
|
|
9
|
+
|
|
10
|
+
function relayUrl() {
|
|
11
|
+
if (process.env.RELAY_URL) return process.env.RELAY_URL;
|
|
12
|
+
try {
|
|
13
|
+
const cfg = join(homedir(), ".agent-bus", "config.json");
|
|
14
|
+
if (existsSync(cfg)) { const u = JSON.parse(readFileSync(cfg, "utf8")).url; if (u) return u; }
|
|
15
|
+
} catch {}
|
|
16
|
+
return "http://127.0.0.1:4477";
|
|
17
|
+
}
|
|
18
|
+
function readStdin() {
|
|
19
|
+
return new Promise(res => { let d = ""; process.stdin.setEncoding("utf8");
|
|
20
|
+
process.stdin.on("data", c => (d += c)); process.stdin.on("end", () => res(d));
|
|
21
|
+
setTimeout(() => res(d), 200); });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
26
|
+
// Mirror sessionstart/heartbeat: home-directory sessions aren't project work — don't card them
|
|
27
|
+
// (would spawn a phantom "<username>" board). Opt in with RELAY_SESSION/RELAY_PROJECT.
|
|
28
|
+
if (!process.env.RELAY_SESSION && !process.env.RELAY_PROJECT && projectDir === homedir()) return;
|
|
29
|
+
|
|
30
|
+
let input = {};
|
|
31
|
+
try { input = JSON.parse((await readStdin()) || "{}"); } catch { return; }
|
|
32
|
+
if (input.tool_name && input.tool_name !== "TodoWrite") return; // the matcher should scope us, but be safe
|
|
33
|
+
const todos = input.tool_input?.todos;
|
|
34
|
+
if (!Array.isArray(todos) || !todos.length) return;
|
|
35
|
+
|
|
36
|
+
// Identity EXACTLY as mcp.mjs/heartbeat resolve it, so we card the same peer the relay registered.
|
|
37
|
+
const project = process.env.RELAY_PROJECT || basename(projectDir);
|
|
38
|
+
const session = process.env.RELAY_SESSION
|
|
39
|
+
|| (process.env.RELAY_AGENT ? `${process.env.RELAY_AGENT}:${project}` : `${hostname()}:${project}`);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await fetch(`${relayUrl()}/todos`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "content-type": "application/json" },
|
|
45
|
+
body: JSON.stringify({ session, project, by: session, todos: todos.map(t => ({ content: t.content, status: t.status })) }),
|
|
46
|
+
signal: AbortSignal.timeout(1500),
|
|
47
|
+
});
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Never block or break the tool flow: swallow everything, always exit clean.
|
|
52
|
+
main().catch(() => {}).finally(() => process.exit(0));
|
package/hub.mjs
CHANGED
|
@@ -189,6 +189,44 @@ const server = http.createServer(async (req, res) => {
|
|
|
189
189
|
appendCardEvent(eventType, t, b.by, eventFrom, eventTo);
|
|
190
190
|
t.updated = now(); dirty = true; return json(res, 200, { ok: true, task: t });
|
|
191
191
|
}
|
|
192
|
+
// Mirror a session's TodoWrite list onto its board as cards, so SOLO work (no crew) shows up live
|
|
193
|
+
// and accrues timeline history. pending/in_progress/completed -> todo/doing/done. Reconciled by
|
|
194
|
+
// todo text per session: present todos create/update; a vanished todo's card is deleted UNLESS it
|
|
195
|
+
// was already done (accomplished work stays in the DONE column). Posted by hooks/todo-sync.mjs.
|
|
196
|
+
if (req.method === "POST" && P === "/todos") {
|
|
197
|
+
const b = await body(req);
|
|
198
|
+
const session = String(b.session || b.by || "").slice(0, 120);
|
|
199
|
+
const project = String(b.project || "").slice(0, 80);
|
|
200
|
+
if (!session || !project) return json(res, 400, { error: "session and project required" });
|
|
201
|
+
touch(session, undefined, project);
|
|
202
|
+
const ST = { pending: "todo", in_progress: "doing", completed: "done" };
|
|
203
|
+
const todos = Array.isArray(b.todos) ? b.todos : [];
|
|
204
|
+
const mine = state.tasks.filter(t => t.source === "todo" && t.assignee === session && t.project === project);
|
|
205
|
+
const seen = new Set();
|
|
206
|
+
for (const todo of todos) {
|
|
207
|
+
const key = String(todo?.content || "").trim().slice(0, 200);
|
|
208
|
+
if (!key) continue;
|
|
209
|
+
seen.add(key);
|
|
210
|
+
const want = ST[todo.status] || "todo";
|
|
211
|
+
let t = mine.find(c => c.todoKey === key);
|
|
212
|
+
if (!t) {
|
|
213
|
+
t = { id: ++state.taskSeq, project, title: key, assignee: session, status: want, difficulty: "", model: "",
|
|
214
|
+
deps: [], by: session, ts: now(), updated: now(), source: "todo", todoKey: key,
|
|
215
|
+
history: [{ to: want, by: session, ts: now() }] };
|
|
216
|
+
state.tasks.push(t); appendCardEvent("created", t, session, null, want); dirty = true;
|
|
217
|
+
} else if (t.status !== want) {
|
|
218
|
+
(t.history ||= []).push({ from: t.status, to: want, by: session, ts: now() });
|
|
219
|
+
if (t.history.length > 40) t.history.splice(0, 10);
|
|
220
|
+
appendCardEvent("moved", t, session, t.status, want); t.status = want; t.updated = now(); dirty = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const t of mine) {
|
|
224
|
+
if (seen.has(t.todoKey) || t.status === "done") continue; // keep accomplished work on the board
|
|
225
|
+
state.tasks = state.tasks.filter(x => x.id !== t.id); appendCardEvent("deleted", t, session, null, null); dirty = true;
|
|
226
|
+
}
|
|
227
|
+
if (state.tasks.length > 2000) state.tasks.splice(0, state.tasks.length - 2000);
|
|
228
|
+
return json(res, 200, { ok: true, count: todos.length });
|
|
229
|
+
}
|
|
192
230
|
if (req.method === "GET" && P === "/tasks") {
|
|
193
231
|
const proj = q.project; const ts = proj ? state.tasks.filter(t => t.project === proj) : state.tasks;
|
|
194
232
|
return json(res, 200, { tasks: ts });
|