teamwork-os 2.0.2
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 +21 -0
- package/README.md +7 -0
- package/bin/teamos.js +3389 -0
- package/package.json +27 -0
- package/packages/gateway/dist/index.js +12357 -0
- package/packages/gateway/dist/mcp/gateway-server.js +390 -0
- package/packages/gateway/package.json +6 -0
- package/web-dist/assets/index-B77BLJdO.js +18 -0
- package/web-dist/assets/index-DkZru7lE.css +1 -0
- package/web-dist/index.html +14 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// packages/gateway/dist/mcp/gateway-server.js
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
|
|
6
|
+
// packages/gateway/dist/mcp/tools.js
|
|
7
|
+
var GATEWAY_TOOLS = [
|
|
8
|
+
{
|
|
9
|
+
name: "whoami",
|
|
10
|
+
description: "Report this session's own runtime identity from first-party gateway data: the Job that dispatched it, if any. sessionId is null for job runs and dry-runs (the spawn env carries no session id today). Use this to confirm where you are running before acting on that assumption.",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {}
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "get_status",
|
|
18
|
+
description: "Gateway + scheduled-jobs summary: gateway version and uptime, the scheduler pause state, and per-job {id, name, schedule, enabled, lastStatus, lastRun, nextRun}.",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
// Wire name kept from the legacy gateway even though CONTEXT.md bans
|
|
26
|
+
// 'cron' as a synonym for Job: `teamos migrate` imports legacy job
|
|
27
|
+
// prompts that call create_cron_job by name, and a rename would silently
|
|
28
|
+
// break every one of them. A documented compat exception, same as the legacy wire
|
|
29
|
+
// names in shared events.ts — every NEW surface says "Job".
|
|
30
|
+
name: "create_cron_job",
|
|
31
|
+
description: "Create a scheduled Job. Same validation path as POST /api/jobs: id (slug), schedule (5-field cron), and exactly one of prompt or command are required; optional name, description, timezone, timeoutSec, catchUp ('run-once' | 'skip'), retentionDays, enabled \u2014 plus the run-identity fields a Job carries itself: model, permissionMode, branch, useWorktree, persona, spendCapUsd.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
id: { type: "string", description: "Job id \u2014 letters, numbers, hyphens (also the JSON filename stem)" },
|
|
36
|
+
name: { type: "string", description: "Display name (defaults to the id)" },
|
|
37
|
+
description: { type: "string", description: "What the job does" },
|
|
38
|
+
schedule: { type: "string", description: '5-field cron expression, e.g. "0 9 * * 1-5"' },
|
|
39
|
+
timezone: { type: "string", description: "IANA timezone for the schedule (default: host timezone)" },
|
|
40
|
+
prompt: { type: "string", description: "Claude prompt \u2014 mutually exclusive with command" },
|
|
41
|
+
command: { type: "string", description: "Shell command \u2014 mutually exclusive with prompt" },
|
|
42
|
+
cwd: { type: "string", description: "Absolute working directory for the run" },
|
|
43
|
+
timeoutSec: { type: "number", description: "Wall-clock budget per run in seconds (default 900)" },
|
|
44
|
+
model: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "Capability alias (low, medium, high, elite, legendary) or a concrete model id. Omit for the engine default."
|
|
47
|
+
},
|
|
48
|
+
permissionMode: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Permission posture of the spawned session: 'bypassPermissions' (default \u2014 what every job does today), 'acceptEdits', 'default', or 'plan'. Headless runs have nobody to prompt, so the non-bypass modes DENY un-permitted tool calls rather than asking."
|
|
51
|
+
},
|
|
52
|
+
branch: { type: "string", description: "Git ref the run works from (base for the worktree when useWorktree is set)" },
|
|
53
|
+
useWorktree: {
|
|
54
|
+
type: "boolean",
|
|
55
|
+
description: "Run in a throwaway git worktree cut from branch instead of directly in cwd (default false)"
|
|
56
|
+
},
|
|
57
|
+
persona: { type: "string", description: "Persona text appended to the system prompt on every turn" },
|
|
58
|
+
spendCapUsd: { type: "number", description: "Per-calendar-day spend cap in USD; reaching it skips this job's dispatch until the next day" },
|
|
59
|
+
catchUp: { type: "string", description: "Missed-run policy: 'run-once' (default) or 'skip'" },
|
|
60
|
+
retentionDays: { type: "number", description: "Per-job artifact retention override in days" },
|
|
61
|
+
enabled: { type: "boolean", description: "Whether the schedule fires (default true)" }
|
|
62
|
+
},
|
|
63
|
+
required: ["id", "schedule"]
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "register_slack_root_post",
|
|
68
|
+
description: "Post a message to Slack as the gateway bot and register it as this run's ROOT post. Everything people reply in that thread accumulates into one session, spawned with CT_JOB_ID set to this jobId, and replies need no @-mention. Pass threadTs to add to an existing root instead of opening a new thread. The channel must be one the gateway is configured for (mapped in channels.directory, or one the connector has already dispatched a turn in).",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
channel: { type: "string", description: "Slack channel id, e.g. C0123456789" },
|
|
73
|
+
text: { type: "string", description: "Message body (Markdown is converted to Slack mrkdwn)" },
|
|
74
|
+
jobId: { type: "string", description: "Job this post belongs to" },
|
|
75
|
+
threadTs: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Existing root ts to post into. Omit to start a new thread."
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
required: ["channel", "text", "jobId"]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// packages/gateway/dist/mcp/api-client.js
|
|
86
|
+
import fs from "node:fs";
|
|
87
|
+
import os from "node:os";
|
|
88
|
+
import path from "node:path";
|
|
89
|
+
|
|
90
|
+
// packages/shared/dist/env-file.js
|
|
91
|
+
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
92
|
+
function parseEnvFile(raw) {
|
|
93
|
+
const entries = [];
|
|
94
|
+
for (const rawLine of raw.split("\n")) {
|
|
95
|
+
const line = rawLine.trim();
|
|
96
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
97
|
+
continue;
|
|
98
|
+
const eq = line.indexOf("=");
|
|
99
|
+
if (eq === -1)
|
|
100
|
+
continue;
|
|
101
|
+
const key = line.slice(0, eq).trim();
|
|
102
|
+
if (!KEY_RE.test(key))
|
|
103
|
+
continue;
|
|
104
|
+
entries.push({ key, value: stripQuotes(line.slice(eq + 1).trim()) });
|
|
105
|
+
}
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
function stripQuotes(raw) {
|
|
109
|
+
if (raw.length >= 2) {
|
|
110
|
+
if (raw.startsWith("'") && raw.endsWith("'")) {
|
|
111
|
+
return raw.slice(1, -1).replace(/'\\''/g, "'");
|
|
112
|
+
}
|
|
113
|
+
if (raw.startsWith('"') && raw.endsWith('"')) {
|
|
114
|
+
return raw.slice(1, -1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return raw;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// packages/shared/dist/model-aliases.js
|
|
121
|
+
var DEFAULT_MODEL_ALIASES = {
|
|
122
|
+
legendary: { model: "claude-fable-5", effortLevel: "xhigh" },
|
|
123
|
+
elite: { model: "claude-opus-5", effortLevel: "xhigh" },
|
|
124
|
+
high: { model: "claude-opus-5", effortLevel: "medium" },
|
|
125
|
+
medium: { model: "claude-sonnet-5", effortLevel: "medium" },
|
|
126
|
+
low: { model: "claude-haiku-4-5-20251001" }
|
|
127
|
+
};
|
|
128
|
+
var MODEL_ALIAS_LABELS = {
|
|
129
|
+
legendary: "Legendary",
|
|
130
|
+
elite: "Elite",
|
|
131
|
+
high: "High",
|
|
132
|
+
medium: "Medium",
|
|
133
|
+
low: "Low"
|
|
134
|
+
};
|
|
135
|
+
function formatModelAliasTarget(target) {
|
|
136
|
+
const model = target.model.replace(/^claude-/, "");
|
|
137
|
+
return target.effortLevel ? `${model} @ ${target.effortLevel}` : model;
|
|
138
|
+
}
|
|
139
|
+
var MODEL_ALIAS_OPTIONS = [
|
|
140
|
+
"legendary",
|
|
141
|
+
"elite",
|
|
142
|
+
"high",
|
|
143
|
+
"medium",
|
|
144
|
+
"low"
|
|
145
|
+
].map((value) => {
|
|
146
|
+
const alias = value;
|
|
147
|
+
return {
|
|
148
|
+
value: alias,
|
|
149
|
+
label: `${MODEL_ALIAS_LABELS[alias]} \xB7 ${formatModelAliasTarget(DEFAULT_MODEL_ALIASES[alias])}`
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// packages/shared/dist/claude-jsonl/normalize.js
|
|
154
|
+
var MAX_TOOL_RESULT_BYTES = 64 * 1024;
|
|
155
|
+
|
|
156
|
+
// packages/shared/dist/migrate/plan.js
|
|
157
|
+
var TIMEOUT_SEC_MAX = 6 * 60 * 60;
|
|
158
|
+
var PERSONA_MAX_LEN = 64 * 1024;
|
|
159
|
+
|
|
160
|
+
// packages/gateway/dist/mcp/api-client.js
|
|
161
|
+
var SESSION_COOKIE_NAME = "__Host-session";
|
|
162
|
+
var GATEWAY_URL = process.env.CT_GATEWAY_URL || "http://127.0.0.1:7463";
|
|
163
|
+
var cachedSessionCookie = null;
|
|
164
|
+
function teamworkHome() {
|
|
165
|
+
return process.env.TEAMWORK_HOME ?? path.join(os.homedir(), ".teamwork");
|
|
166
|
+
}
|
|
167
|
+
function readAuthToken() {
|
|
168
|
+
const fromEnv = process.env.AUTH_TOKEN?.trim();
|
|
169
|
+
if (fromEnv)
|
|
170
|
+
return fromEnv;
|
|
171
|
+
try {
|
|
172
|
+
const envPath = path.join(teamworkHome(), ".env");
|
|
173
|
+
if (!fs.existsSync(envPath))
|
|
174
|
+
return null;
|
|
175
|
+
const entry = parseEnvFile(fs.readFileSync(envPath, "utf8")).find((e) => e.key === "AUTH_TOKEN");
|
|
176
|
+
const v = entry?.value.trim();
|
|
177
|
+
return v && v.length > 0 ? v : null;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
const code = err.code;
|
|
180
|
+
if (code !== "ENOENT") {
|
|
181
|
+
console.warn(`gateway-mcp: could not read AUTH_TOKEN from the teamwork env file (${code ?? "unknown"}): ${err.message}`);
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function loginForCookie(base, token) {
|
|
187
|
+
try {
|
|
188
|
+
const res = await fetch(`${base}/api/auth/login`, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { "Content-Type": "application/json" },
|
|
191
|
+
body: JSON.stringify({ token })
|
|
192
|
+
});
|
|
193
|
+
if (!res.ok) {
|
|
194
|
+
console.warn(`gateway-mcp: AUTH_TOKEN rejected by gateway login (HTTP ${res.status}); requests will fail`);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const setCookie = res.headers.get("set-cookie");
|
|
198
|
+
if (!setCookie) {
|
|
199
|
+
console.warn("gateway-mcp: gateway login returned 2xx but no Set-Cookie header; requests will fail");
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const m = setCookie.match(new RegExp(`${SESSION_COOKIE_NAME}=([^;]+)`));
|
|
203
|
+
if (!m) {
|
|
204
|
+
console.warn(`gateway-mcp: gateway Set-Cookie did not contain ${SESSION_COOKIE_NAME}; requests will fail`);
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
return m[1];
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.warn(`gateway-mcp: gateway login request failed at ${base}/api/auth/login: ${err.message}`);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function authenticatedFetch(url, init) {
|
|
214
|
+
const token = readAuthToken();
|
|
215
|
+
if (!token)
|
|
216
|
+
return fetch(url, init);
|
|
217
|
+
const base = new URL(url).origin;
|
|
218
|
+
if (!cachedSessionCookie) {
|
|
219
|
+
cachedSessionCookie = await loginForCookie(base, token);
|
|
220
|
+
}
|
|
221
|
+
const send = async (cookie) => {
|
|
222
|
+
const headers = new Headers(init?.headers);
|
|
223
|
+
if (cookie)
|
|
224
|
+
headers.set("Cookie", `${SESSION_COOKIE_NAME}=${cookie}`);
|
|
225
|
+
return fetch(url, { ...init, headers });
|
|
226
|
+
};
|
|
227
|
+
const first = await send(cachedSessionCookie);
|
|
228
|
+
if (first.status === 401 && cachedSessionCookie !== null) {
|
|
229
|
+
cachedSessionCookie = await loginForCookie(base, token);
|
|
230
|
+
return send(cachedSessionCookie);
|
|
231
|
+
}
|
|
232
|
+
return first;
|
|
233
|
+
}
|
|
234
|
+
async function apiGet(apiPath) {
|
|
235
|
+
const res = await authenticatedFetch(`${GATEWAY_URL}${apiPath}`);
|
|
236
|
+
if (!res.ok) {
|
|
237
|
+
const text = await res.text().catch(() => "");
|
|
238
|
+
throw new Error(`API ${apiPath}: ${res.status} ${text || res.statusText}`);
|
|
239
|
+
}
|
|
240
|
+
return res.json();
|
|
241
|
+
}
|
|
242
|
+
async function apiPost(apiPath, body) {
|
|
243
|
+
const res = await authenticatedFetch(`${GATEWAY_URL}${apiPath}`, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
headers: { "Content-Type": "application/json" },
|
|
246
|
+
body: JSON.stringify(body)
|
|
247
|
+
});
|
|
248
|
+
if (!res.ok) {
|
|
249
|
+
const text = await res.text().catch(() => "");
|
|
250
|
+
throw new Error(`API ${apiPath}: ${res.status} ${text || res.statusText}`);
|
|
251
|
+
}
|
|
252
|
+
return res.json();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// packages/gateway/dist/mcp/gateway-server.js
|
|
256
|
+
async function handleTool(name, args) {
|
|
257
|
+
switch (name) {
|
|
258
|
+
case "whoami": {
|
|
259
|
+
const sessionId = process.env.CT_SESSION_ID;
|
|
260
|
+
const jobId = process.env.CT_JOB_ID;
|
|
261
|
+
if (!sessionId && !jobId) {
|
|
262
|
+
return "Error: no session context available (CT_SESSION_ID / CT_JOB_ID are not set for this MCP server)";
|
|
263
|
+
}
|
|
264
|
+
const identity = {
|
|
265
|
+
sessionId: sessionId ?? null,
|
|
266
|
+
jobId: jobId ?? null
|
|
267
|
+
};
|
|
268
|
+
if (sessionId && /^[a-zA-Z0-9_-]{8,64}$/.test(sessionId)) {
|
|
269
|
+
try {
|
|
270
|
+
const session = await apiGet(`/api/sessions/${sessionId}`);
|
|
271
|
+
const s = session.session ?? session;
|
|
272
|
+
identity.status = s.status ?? null;
|
|
273
|
+
identity.cwd = s.cwd ?? null;
|
|
274
|
+
identity.title = s.title ?? null;
|
|
275
|
+
} catch (err) {
|
|
276
|
+
identity.sessionLookupError = err instanceof Error ? err.message : String(err);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return JSON.stringify(identity);
|
|
280
|
+
}
|
|
281
|
+
case "get_status": {
|
|
282
|
+
const status = await apiGet("/api/scheduler");
|
|
283
|
+
return JSON.stringify(status);
|
|
284
|
+
}
|
|
285
|
+
case "create_cron_job": {
|
|
286
|
+
const hasPrompt = typeof args.prompt === "string" && args.prompt.trim().length > 0;
|
|
287
|
+
const hasCommand = typeof args.command === "string" && args.command.trim().length > 0;
|
|
288
|
+
if (!hasPrompt && !hasCommand) {
|
|
289
|
+
return "Error: exactly one of prompt or command is required";
|
|
290
|
+
}
|
|
291
|
+
const result = await apiPost("/api/jobs", { enabled: true, ...args });
|
|
292
|
+
return JSON.stringify(result);
|
|
293
|
+
}
|
|
294
|
+
case "register_slack_root_post": {
|
|
295
|
+
const result = await apiPost("/api/slack/root-post", args);
|
|
296
|
+
return JSON.stringify(result);
|
|
297
|
+
}
|
|
298
|
+
default:
|
|
299
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function sendResponse(response) {
|
|
303
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
304
|
+
}
|
|
305
|
+
async function handleRequest(request) {
|
|
306
|
+
const { id, method, params } = request;
|
|
307
|
+
switch (method) {
|
|
308
|
+
case "initialize":
|
|
309
|
+
sendResponse({
|
|
310
|
+
jsonrpc: "2.0",
|
|
311
|
+
id,
|
|
312
|
+
result: {
|
|
313
|
+
protocolVersion: "2024-11-05",
|
|
314
|
+
capabilities: { tools: {} },
|
|
315
|
+
serverInfo: {
|
|
316
|
+
name: "teamwork-os-gateway",
|
|
317
|
+
version: "0.2.0"
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
break;
|
|
322
|
+
case "tools/list":
|
|
323
|
+
sendResponse({
|
|
324
|
+
jsonrpc: "2.0",
|
|
325
|
+
id,
|
|
326
|
+
result: { tools: GATEWAY_TOOLS }
|
|
327
|
+
});
|
|
328
|
+
break;
|
|
329
|
+
case "tools/call": {
|
|
330
|
+
const toolName = params?.name;
|
|
331
|
+
const toolArgs = params?.arguments || {};
|
|
332
|
+
try {
|
|
333
|
+
const result = await handleTool(toolName, toolArgs);
|
|
334
|
+
sendResponse({
|
|
335
|
+
jsonrpc: "2.0",
|
|
336
|
+
id,
|
|
337
|
+
result: {
|
|
338
|
+
content: [{ type: "text", text: result }]
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
} catch (err) {
|
|
342
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
343
|
+
sendResponse({
|
|
344
|
+
jsonrpc: "2.0",
|
|
345
|
+
id,
|
|
346
|
+
result: {
|
|
347
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
348
|
+
isError: true
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case "notifications/initialized":
|
|
355
|
+
break;
|
|
356
|
+
default:
|
|
357
|
+
sendResponse({
|
|
358
|
+
jsonrpc: "2.0",
|
|
359
|
+
id,
|
|
360
|
+
error: { code: -32601, message: `Method not found: ${method}` }
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (process.env.TEAMOS_MCP_SERVER_NO_LISTEN !== "1") {
|
|
365
|
+
const rl = createInterface({ input: process.stdin });
|
|
366
|
+
rl.on("line", (line) => {
|
|
367
|
+
let request;
|
|
368
|
+
try {
|
|
369
|
+
request = JSON.parse(line);
|
|
370
|
+
} catch {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
handleRequest(request).catch((err) => {
|
|
374
|
+
sendResponse({
|
|
375
|
+
jsonrpc: "2.0",
|
|
376
|
+
id: request.id,
|
|
377
|
+
error: {
|
|
378
|
+
code: -32603,
|
|
379
|
+
message: err instanceof Error ? err.message : String(err)
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
rl.on("close", () => {
|
|
385
|
+
process.exit(0);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
export {
|
|
389
|
+
handleTool
|
|
390
|
+
};
|