traintrack 2.0.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/LICENSE +201 -0
- package/README.md +158 -0
- package/dist/channel/channel.js +64 -0
- package/dist/channel/resolve.js +43 -0
- package/dist/cli.js +164 -0
- package/dist/index.js +6 -0
- package/dist/mcp/server.js +198 -0
- package/dist/mcp/tools.js +207 -0
- package/dist/mcp-server.js +8 -0
- package/dist/onboarding/briefing.js +24 -0
- package/dist/runner/argv.js +47 -0
- package/dist/runner/event-parser.js +165 -0
- package/dist/runner/turn-runner.js +81 -0
- package/dist/setup/blocks.js +251 -0
- package/dist/setup/configure.js +179 -0
- package/dist/setup/detect.js +53 -0
- package/dist/setup/harness.js +61 -0
- package/dist/setup/prompt.js +218 -0
- package/dist/setup/setup.js +106 -0
- package/dist/setup/types.js +9 -0
- package/dist/spawn/spawn.js +90 -0
- package/dist/ui/banner.js +76 -0
- package/dist/worker/worker.js +190 -0
- package/hooks/hooks-codex.json +16 -0
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks.json +16 -0
- package/hooks/run-hook.cmd +46 -0
- package/hooks/session-start +44 -0
- package/hooks/session-start-codex +28 -0
- package/package.json +52 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// ─── traintrack headless worker ──────────────────────────────────────────────
|
|
2
|
+
// A long-lived loop that makes ONE agent a reliable, auto-responding teammate.
|
|
3
|
+
// Unlike PTY injection (best-effort, research-proven unreliable), this drains the
|
|
4
|
+
// agent's durable inbox (the local SQLite Channel) and runs each turn as a fresh
|
|
5
|
+
// HEADLESS process (claude --print / codex exec resume), using the structured
|
|
6
|
+
// turn-end event as the ACK. The receiving agent therefore "sees" messages with
|
|
7
|
+
// zero human input and on a guaranteed schedule.
|
|
8
|
+
//
|
|
9
|
+
// Ported from $TT/src/cli/headless-worker.ts and REPOINTED off Orca's
|
|
10
|
+
// RuntimeClient (RPC) onto the local Channel:
|
|
11
|
+
// orchestration.check {unread} → channel.getUnread(handle) + channel.markRead(ids)
|
|
12
|
+
// orchestration.send {to,from,body} → channel.insertMessage({to,from,body})
|
|
13
|
+
// team.join {...} → channel.addMember({handle, agent, role, kind:'headless', ...})
|
|
14
|
+
// team.list → channel.listMembers()
|
|
15
|
+
// No RPC, no engine — the worker takes a Channel directly.
|
|
16
|
+
import { buildHeadlessArgv } from '../runner/argv.js';
|
|
17
|
+
import { runHeadlessTurn } from '../runner/turn-runner.js';
|
|
18
|
+
import { buildBriefing } from '../onboarding/briefing.js';
|
|
19
|
+
/** Build the team briefing from the channel's CURRENT roster. Called every loop
|
|
20
|
+
* iteration so a worker's prompt reflects teammates who joined after it spawned. */
|
|
21
|
+
export function buildRosterBriefing(channel, selfHandle, selfRole, teamName = 'team') {
|
|
22
|
+
const roster = channel.listMembers().map((m) => ({
|
|
23
|
+
handle: m.handle,
|
|
24
|
+
role: m.role,
|
|
25
|
+
agent: m.agent,
|
|
26
|
+
kind: m.kind,
|
|
27
|
+
}));
|
|
28
|
+
return buildBriefing({ teamName, selfHandle, selfRole, roster });
|
|
29
|
+
}
|
|
30
|
+
/** Build the turn prompt from the drained inbox messages (port of the rust build_prompt). */
|
|
31
|
+
export function buildWorkerPrompt(agent, messages, briefing) {
|
|
32
|
+
const lines = messages.map((m) => `From ${m.from}: ${m.body}`).join('\n');
|
|
33
|
+
const body = [
|
|
34
|
+
`You are the "${agent}" agent in a traintrack multi-agent workspace, coordinating with teammates over a shared message channel.`,
|
|
35
|
+
`The following message(s) just arrived in your inbox. Read them and reply — your reply is delivered back to the sender(s).`,
|
|
36
|
+
'',
|
|
37
|
+
'--- Messages ---',
|
|
38
|
+
lines,
|
|
39
|
+
'--- End ---',
|
|
40
|
+
'',
|
|
41
|
+
'Respond now.'
|
|
42
|
+
].join('\n');
|
|
43
|
+
if (briefing) {
|
|
44
|
+
return `${briefing}\n\n${body}`;
|
|
45
|
+
}
|
|
46
|
+
return body;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a direct peer address from a worker's reply text. If `text` starts with
|
|
50
|
+
* `@`, the leading token (up to the first whitespace) is matched case-insensitively
|
|
51
|
+
* as a substring against each member's `handle` or `role` (excluding `selfHandle`).
|
|
52
|
+
* On a match, returns `{ to: matchedHandle, body: <rest of text> }`. If the text has
|
|
53
|
+
* no leading `@`, or the token matches no member, returns `null` (caller falls back
|
|
54
|
+
* to replying to the original sender — the message is never dropped). Ported from the
|
|
55
|
+
* `@name` parse in traintrack-desktop's session.ts.
|
|
56
|
+
*/
|
|
57
|
+
export function resolvePeerAddress(text, members, selfHandle) {
|
|
58
|
+
if (!text.startsWith('@')) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const ws = text.search(/\s/);
|
|
62
|
+
const token = (ws === -1 ? text.slice(1) : text.slice(1, ws)).trim();
|
|
63
|
+
const body = ws === -1 ? '' : text.slice(ws + 1).trim();
|
|
64
|
+
if (!token) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const needle = token.toLowerCase();
|
|
68
|
+
const match = members.find((m) => m.handle !== selfHandle &&
|
|
69
|
+
(m.handle.toLowerCase().includes(needle) || m.role.toLowerCase().includes(needle)));
|
|
70
|
+
return match ? { to: match.handle, body } : null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* One drain → headless turn → reply cycle. Pulls this worker's unread inbox from
|
|
74
|
+
* the channel, runs a single headless agent turn with those messages spliced in,
|
|
75
|
+
* posts the agent's reply back to each distinct sender, and marks the drained
|
|
76
|
+
* messages read. Returns what happened (for logging + tests) including the
|
|
77
|
+
* (possibly new) session id to carry forward.
|
|
78
|
+
*/
|
|
79
|
+
export async function runWorkerCycle(deps) {
|
|
80
|
+
const { channel, handle, agent, cwd, model, runTurn, print, briefing } = deps;
|
|
81
|
+
const messages = channel.getUnread(handle);
|
|
82
|
+
if (messages.length === 0) {
|
|
83
|
+
return { processed: 0, sessionId: deps.sessionId, repliedTo: [] };
|
|
84
|
+
}
|
|
85
|
+
print?.(`[worker] ${agent} draining ${messages.length} message(s)`);
|
|
86
|
+
const prompt = buildWorkerPrompt(agent, messages, briefing);
|
|
87
|
+
const turn = await runTurn({
|
|
88
|
+
provider: agent,
|
|
89
|
+
prompt,
|
|
90
|
+
cwd,
|
|
91
|
+
model,
|
|
92
|
+
resumeSessionId: deps.sessionId
|
|
93
|
+
});
|
|
94
|
+
const reply = turn.finalText.trim();
|
|
95
|
+
const senders = [...new Set(messages.map((m) => m.from).filter(Boolean))];
|
|
96
|
+
// If the reply opens with `@<handle-or-role>`, route the rest to that teammate
|
|
97
|
+
// instead of the original sender(s). No `@` / no match → reply to senders as usual.
|
|
98
|
+
const peer = reply ? resolvePeerAddress(reply, channel.listMembers(), handle) : null;
|
|
99
|
+
let repliedTo = [];
|
|
100
|
+
if (reply && peer) {
|
|
101
|
+
channel.insertMessage({ to: peer.to, from: handle, body: peer.body, type: 'status' });
|
|
102
|
+
repliedTo = [peer.to];
|
|
103
|
+
print?.(`[worker] ${agent} addressed ${peer.to}`);
|
|
104
|
+
}
|
|
105
|
+
else if (reply) {
|
|
106
|
+
for (const to of senders) {
|
|
107
|
+
channel.insertMessage({ to, from: handle, body: reply, type: 'status' });
|
|
108
|
+
}
|
|
109
|
+
repliedTo = senders;
|
|
110
|
+
print?.(`[worker] ${agent} replied to ${senders.join(', ')}`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
print?.(`[worker] ${agent} produced no reply text (isError=${turn.isError})`);
|
|
114
|
+
}
|
|
115
|
+
// ACK the drained messages so they are not re-processed next cycle. Done after
|
|
116
|
+
// the turn + reply so a crash mid-turn leaves them unread (re-tried) rather
|
|
117
|
+
// than silently dropped.
|
|
118
|
+
channel.markRead(messages.map((m) => m.id));
|
|
119
|
+
return {
|
|
120
|
+
processed: messages.length,
|
|
121
|
+
reply: reply || undefined,
|
|
122
|
+
sessionId: turn.sessionId ?? deps.sessionId,
|
|
123
|
+
repliedTo
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
127
|
+
/** Default turn runner: build the argv then spawn a real headless turn. */
|
|
128
|
+
const realTurnRunner = async (input) => {
|
|
129
|
+
const { command, args } = buildHeadlessArgv({
|
|
130
|
+
agent: input.provider,
|
|
131
|
+
prompt: input.prompt,
|
|
132
|
+
model: input.model,
|
|
133
|
+
resumeSessionId: input.resumeSessionId
|
|
134
|
+
});
|
|
135
|
+
const outcome = await runHeadlessTurn({
|
|
136
|
+
provider: input.provider,
|
|
137
|
+
command,
|
|
138
|
+
args,
|
|
139
|
+
cwd: input.cwd
|
|
140
|
+
});
|
|
141
|
+
return { finalText: outcome.finalText, sessionId: outcome.sessionId, isError: outcome.isError };
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Run the worker loop until the process is killed (or one cycle when once=true).
|
|
145
|
+
* On start it registers itself in the channel's roster (addMember, kind=headless),
|
|
146
|
+
* reads the roster (listMembers), and builds the team briefing ONCE — the briefing
|
|
147
|
+
* is then prepended to every per-turn prompt.
|
|
148
|
+
*/
|
|
149
|
+
export async function runWorker(opts) {
|
|
150
|
+
const print = opts.print ?? ((msg) => process.stderr.write(`${msg}\n`));
|
|
151
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
152
|
+
const pollMs = opts.pollMs ?? 3000;
|
|
153
|
+
const runTurn = opts.runTurn ?? realTurnRunner;
|
|
154
|
+
// Self-register in the channel roster so teammates discover this worker.
|
|
155
|
+
const self = {
|
|
156
|
+
handle: opts.handle,
|
|
157
|
+
agent: opts.agent,
|
|
158
|
+
role: opts.role,
|
|
159
|
+
kind: opts.kind ?? 'headless',
|
|
160
|
+
status: 'active',
|
|
161
|
+
worktree: opts.cwd
|
|
162
|
+
};
|
|
163
|
+
opts.channel.addMember(self);
|
|
164
|
+
print(`[worker] online as ${opts.handle} (agent=${opts.agent}, role=${opts.role}, cwd=${opts.cwd}, poll=${pollMs}ms)`);
|
|
165
|
+
let sessionId;
|
|
166
|
+
for (;;) {
|
|
167
|
+
try {
|
|
168
|
+
const briefing = buildRosterBriefing(opts.channel, opts.handle, opts.role);
|
|
169
|
+
const cycle = await runWorkerCycle({
|
|
170
|
+
channel: opts.channel,
|
|
171
|
+
handle: opts.handle,
|
|
172
|
+
agent: opts.agent,
|
|
173
|
+
cwd: opts.cwd,
|
|
174
|
+
model: opts.model,
|
|
175
|
+
runTurn,
|
|
176
|
+
sessionId,
|
|
177
|
+
print,
|
|
178
|
+
briefing
|
|
179
|
+
});
|
|
180
|
+
sessionId = cycle.sessionId;
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
print(`[worker] cycle error: ${err instanceof Error ? err.message : String(err)}`);
|
|
184
|
+
}
|
|
185
|
+
if (opts.once) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
await sleep(pollMs);
|
|
189
|
+
}
|
|
190
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
: << 'CMDBLOCK'
|
|
2
|
+
@echo off
|
|
3
|
+
REM Cross-platform polyglot wrapper for hook scripts.
|
|
4
|
+
REM On Windows: cmd.exe runs the batch portion, which finds and calls bash.
|
|
5
|
+
REM On Unix: the shell interprets this as a script (: is a no-op in bash).
|
|
6
|
+
REM
|
|
7
|
+
REM Hook scripts use extensionless filenames (e.g. "session-start" not
|
|
8
|
+
REM "session-start.sh") so Claude Code's Windows auto-detection -- which
|
|
9
|
+
REM prepends "bash" to any command containing .sh -- doesn't interfere.
|
|
10
|
+
REM
|
|
11
|
+
REM Usage: run-hook.cmd <script-name> [args...]
|
|
12
|
+
|
|
13
|
+
if "%~1"=="" (
|
|
14
|
+
echo run-hook.cmd: missing script name >&2
|
|
15
|
+
exit /b 1
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
set "HOOK_DIR=%~dp0"
|
|
19
|
+
|
|
20
|
+
REM Try Git for Windows bash in standard locations
|
|
21
|
+
if exist "C:\Program Files\Git\bin\bash.exe" (
|
|
22
|
+
"C:\Program Files\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
|
23
|
+
exit /b %ERRORLEVEL%
|
|
24
|
+
)
|
|
25
|
+
if exist "C:\Program Files (x86)\Git\bin\bash.exe" (
|
|
26
|
+
"C:\Program Files (x86)\Git\bin\bash.exe" "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
|
27
|
+
exit /b %ERRORLEVEL%
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
REM Try bash on PATH (e.g. user-installed Git Bash, MSYS2, Cygwin)
|
|
31
|
+
where bash >nul 2>nul
|
|
32
|
+
if %ERRORLEVEL% equ 0 (
|
|
33
|
+
bash "%HOOK_DIR%%~1" %2 %3 %4 %5 %6 %7 %8 %9
|
|
34
|
+
exit /b %ERRORLEVEL%
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
REM No bash found - exit silently rather than error
|
|
38
|
+
REM (plugin still works, just without SessionStart context injection)
|
|
39
|
+
exit /b 0
|
|
40
|
+
CMDBLOCK
|
|
41
|
+
|
|
42
|
+
# Unix: run the named script directly
|
|
43
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
44
|
+
SCRIPT_NAME="$1"
|
|
45
|
+
shift
|
|
46
|
+
exec bash "${SCRIPT_DIR}/${SCRIPT_NAME}" "$@"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SessionStart hook for the traintrack plugin.
|
|
3
|
+
# Injects "you are the team lead" awareness so the model knows it can spawn,
|
|
4
|
+
# delegate to, and collect from a team of worker agents via the traintrack MCP tools.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# The traintrack awareness content injected into the session context.
|
|
9
|
+
traintrack_content="You have the \`traintrack\` multi-agent coordination tools (list_team, spawn_worker, delegate_task, await_results, send_message, check_messages). You are the team LEAD. When the user asks you to build something sizable, you can spawn worker agents to parallelize: spawn_worker(agent, role, task) for each part, then await_results() to collect their work, then synthesize. Use them when delegation helps; do simple tasks yourself. You can run MULTIPLE rounds: spawn or delegate to teammates by role, call await_results to collect their replies, then delegate follow-up tasks based on what came back and collect again, before synthesizing your final answer. Give each teammate a clear role and keep the team small. If a human adds your session to an EXISTING team (rather than you leading), call join_team(handle, role) first, then check_messages — teammates may message you at any time."
|
|
10
|
+
|
|
11
|
+
# Escape a string for embedding inside a JSON string literal. Each ${s//old/new}
|
|
12
|
+
# is a single C-level pass — orders of magnitude faster than a char loop.
|
|
13
|
+
escape_for_json() {
|
|
14
|
+
local s="$1"
|
|
15
|
+
s="${s//\\/\\\\}"
|
|
16
|
+
s="${s//\"/\\\"}"
|
|
17
|
+
s="${s//$'\n'/\\n}"
|
|
18
|
+
s="${s//$'\r'/\\r}"
|
|
19
|
+
s="${s//$'\t'/\\t}"
|
|
20
|
+
printf '%s' "$s"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
traintrack_escaped=$(escape_for_json "$traintrack_content")
|
|
24
|
+
session_context="<EXTREMELY_IMPORTANT>\n${traintrack_escaped}\n</EXTREMELY_IMPORTANT>"
|
|
25
|
+
|
|
26
|
+
# Output context injection as JSON.
|
|
27
|
+
# Cursor hooks expect additional_context (snake_case).
|
|
28
|
+
# Claude Code hooks expect hookSpecificOutput.additionalContext (nested).
|
|
29
|
+
# Copilot CLI (v1.0.11+) and others expect additionalContext (top-level, SDK standard).
|
|
30
|
+
#
|
|
31
|
+
# Uses printf instead of heredoc to work around bash 5.3+ heredoc hang.
|
|
32
|
+
# See: https://github.com/obra/superpowers/issues/571
|
|
33
|
+
if [ -n "${CURSOR_PLUGIN_ROOT:-}" ]; then
|
|
34
|
+
# Cursor sets CURSOR_PLUGIN_ROOT (may also set CLAUDE_PLUGIN_ROOT)
|
|
35
|
+
printf '{\n "additional_context": "%s"\n}\n' "$session_context" | cat
|
|
36
|
+
elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then
|
|
37
|
+
# Claude Code sets CLAUDE_PLUGIN_ROOT without COPILOT_CLI
|
|
38
|
+
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" | cat
|
|
39
|
+
else
|
|
40
|
+
# Copilot CLI (sets COPILOT_CLI=1) or unknown platform — SDK standard format
|
|
41
|
+
printf '{\n "additionalContext": "%s"\n}\n' "$session_context" | cat
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
exit 0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Codex SessionStart hook for traintrack plugin.
|
|
3
|
+
# Injects "you are the team lead" awareness so the model knows it can spawn,
|
|
4
|
+
# delegate to, and collect from a team of worker agents via the traintrack MCP tools.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
10
|
+
|
|
11
|
+
traintrack_content="You have the \`traintrack\` multi-agent coordination tools (list_team, spawn_worker, delegate_task, await_results, send_message, check_messages). You are the team LEAD. When the user asks you to build something sizable, you can spawn worker agents to parallelize: spawn_worker(agent, role, task) for each part, then await_results() to collect their work, then synthesize. Use them when delegation helps; do simple tasks yourself. You can run MULTIPLE rounds: spawn or delegate to teammates by role, call await_results to collect their replies, then delegate follow-up tasks based on what came back and collect again, before synthesizing your final answer. Give each teammate a clear role and keep the team small. If a human adds your session to an EXISTING team (rather than you leading), call join_team(handle, role) first, then check_messages — teammates may message you at any time."
|
|
12
|
+
|
|
13
|
+
escape_for_json() {
|
|
14
|
+
local s="$1"
|
|
15
|
+
s="${s//\\/\\\\}"
|
|
16
|
+
s="${s//\"/\\\"}"
|
|
17
|
+
s="${s//$'\n'/\\n}"
|
|
18
|
+
s="${s//$'\r'/\\r}"
|
|
19
|
+
s="${s//$'\t'/\\t}"
|
|
20
|
+
printf '%s' "$s"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
traintrack_escaped=$(escape_for_json "$traintrack_content")
|
|
24
|
+
session_context="<EXTREMELY_IMPORTANT>\n${traintrack_escaped}\n</EXTREMELY_IMPORTANT>"
|
|
25
|
+
|
|
26
|
+
printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" | cat
|
|
27
|
+
|
|
28
|
+
exit 0
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "traintrack",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"description": "Multi-agent coordination for coding agents — a lead spawns worker agents, delegates tasks, and collects results over a local channel. One command wires it into Claude Code, Codex, Cursor, and OpenCode.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"multi-agent",
|
|
9
|
+
"claude",
|
|
10
|
+
"codex",
|
|
11
|
+
"cursor",
|
|
12
|
+
"opencode",
|
|
13
|
+
"agent",
|
|
14
|
+
"coordination",
|
|
15
|
+
"a2a",
|
|
16
|
+
"orchestration",
|
|
17
|
+
"ai"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/OKKHALIL3/traintrack#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/OKKHALIL3/traintrack/issues"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/OKKHALIL3/traintrack.git"
|
|
26
|
+
},
|
|
27
|
+
"author": "Omar Khalil",
|
|
28
|
+
"license": "Apache-2.0",
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "dist/index.js",
|
|
31
|
+
"exports": { ".": "./dist/index.js" },
|
|
32
|
+
"bin": { "traintrack": "dist/cli.js" },
|
|
33
|
+
"files": ["dist", "hooks"],
|
|
34
|
+
"engines": { "node": ">=18" },
|
|
35
|
+
"os": ["darwin", "linux"],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.json && npm run postbuild",
|
|
38
|
+
"postbuild": "chmod +x dist/cli.js",
|
|
39
|
+
"prepare": "npm run build",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": { "better-sqlite3": "^11.8.0" },
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
46
|
+
"@types/node": "^22.10.0",
|
|
47
|
+
"typescript": "^5.7.0",
|
|
48
|
+
"vitest": "^2.1.0",
|
|
49
|
+
"eslint": "^9.0.0",
|
|
50
|
+
"prettier": "^3.4.0"
|
|
51
|
+
}
|
|
52
|
+
}
|