too-many-cooks 0.4.0 → 0.4.1
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/build/bin/server.js +1 -0
- package/build/lib/src/admin_routes.js +179 -0
- package/build/lib/src/config.js +10 -0
- package/build/lib/src/data/config.js +42 -0
- package/build/lib/src/data/data.js +6 -0
- package/build/lib/src/data/db.js +637 -0
- package/build/lib/src/data/schema.js +51 -0
- package/build/lib/src/data/types.gen.js +100 -0
- package/build/lib/src/data/types.js +16 -0
- package/build/lib/src/logger.js +57 -0
- package/build/lib/src/notifications.js +121 -0
- package/build/lib/src/result.js +33 -0
- package/build/lib/src/server.js +128 -0
- package/build/lib/src/tools/lock_tool.js +182 -0
- package/build/lib/src/tools/message_tool.js +139 -0
- package/build/lib/src/tools/plan_tool.js +130 -0
- package/build/lib/src/tools/register_tool.js +135 -0
- package/build/lib/src/tools/status_tool.js +49 -0
- package/build/lib/src/tools/tool_utils.js +41 -0
- package/build/lib/src/types.js +9 -0
- package/build/lib/too_many_cooks.js +12 -0
- package/package.json +2 -2
- package/readme.md +2 -2
package/build/bin/server.js
CHANGED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/// Admin REST endpoints for the VSCode extension.
|
|
2
|
+
///
|
|
3
|
+
/// The VSIX talks to these endpoints - never touches the DB directly.
|
|
4
|
+
/// Streamable HTTP endpoint pushes all state changes in real-time.
|
|
5
|
+
import express from "express";
|
|
6
|
+
import { agentIdentityToJson, fileLockToJson, agentPlanToJson, messageToJson, agentRegistrationToJson, dbErrorToJson, } from "./data/data.js";
|
|
7
|
+
/** Logger name for admin push events. */
|
|
8
|
+
const ADMIN_LOGGER_NAME = "too-many-cooks-admin";
|
|
9
|
+
/** Send a push event to all connected admin servers. */
|
|
10
|
+
const sendToServer = async (entry, data, servers, transports) => {
|
|
11
|
+
const [key, server] = entry;
|
|
12
|
+
console.error(`[TMC] [PUSH] Sending to ${key}`);
|
|
13
|
+
try {
|
|
14
|
+
await server.sendLoggingMessage({
|
|
15
|
+
level: "info",
|
|
16
|
+
logger: ADMIN_LOGGER_NAME,
|
|
17
|
+
data,
|
|
18
|
+
});
|
|
19
|
+
console.error(`[TMC] [PUSH] Sent OK to ${key}`);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
console.error(`[TMC] [PUSH] FAILED ${key}`);
|
|
23
|
+
servers.delete(key);
|
|
24
|
+
transports.delete(key);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/** Create an admin event hub for Streamable HTTP push. */
|
|
28
|
+
export const createAdminEventHub = () => {
|
|
29
|
+
const transports = new Map();
|
|
30
|
+
const servers = new Map();
|
|
31
|
+
const pushEvent = (event, payload) => {
|
|
32
|
+
console.error(`[TMC] [PUSH] ${event} -> ${String(servers.size)} server(s), ${String(transports.size)} transport(s)`);
|
|
33
|
+
const data = {
|
|
34
|
+
event,
|
|
35
|
+
timestamp: Date.now(),
|
|
36
|
+
payload,
|
|
37
|
+
};
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
39
|
+
Promise.all([...servers.entries()].map(
|
|
40
|
+
// eslint-disable-next-line require-await
|
|
41
|
+
async (entry) => sendToServer(entry, data, servers, transports)));
|
|
42
|
+
};
|
|
43
|
+
return { transports, servers, pushEvent };
|
|
44
|
+
};
|
|
45
|
+
/** Send an error response. */
|
|
46
|
+
const sendError = (res, code, message) => {
|
|
47
|
+
res.status(code).send(message);
|
|
48
|
+
};
|
|
49
|
+
/** Extract a string field from the request body, or undefined. */
|
|
50
|
+
const stringField = (body, field) => {
|
|
51
|
+
const value = body[field];
|
|
52
|
+
return typeof value === "string" ? value : undefined;
|
|
53
|
+
};
|
|
54
|
+
/** Register GET /admin/status route. */
|
|
55
|
+
const registerStatusRoute = (app, db) => {
|
|
56
|
+
app.get("/admin/status", (_req, res) => {
|
|
57
|
+
const agentsResult = db.listAgents();
|
|
58
|
+
const agents = agentsResult.ok
|
|
59
|
+
? agentsResult.value.map(agentIdentityToJson)
|
|
60
|
+
: [];
|
|
61
|
+
const locksResult = db.listLocks();
|
|
62
|
+
const locks = locksResult.ok
|
|
63
|
+
? locksResult.value.map(fileLockToJson)
|
|
64
|
+
: [];
|
|
65
|
+
const plansResult = db.listPlans();
|
|
66
|
+
const plans = plansResult.ok
|
|
67
|
+
? plansResult.value.map(agentPlanToJson)
|
|
68
|
+
: [];
|
|
69
|
+
const messagesResult = db.listAllMessages();
|
|
70
|
+
const messages = messagesResult.ok
|
|
71
|
+
? messagesResult.value.map(messageToJson)
|
|
72
|
+
: [];
|
|
73
|
+
res
|
|
74
|
+
.set("Content-Type", "application/json")
|
|
75
|
+
.send(JSON.stringify({ agents, locks, plans, messages }));
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
/** Register POST /admin/delete-lock route. */
|
|
79
|
+
const registerDeleteLockRoute = (app, db, hub) => {
|
|
80
|
+
app.post("/admin/delete-lock", (req, res) => {
|
|
81
|
+
const filePath = stringField(req.body, "filePath");
|
|
82
|
+
if (filePath === undefined) {
|
|
83
|
+
sendError(res, 400, "filePath required");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const result = db.adminDeleteLock(filePath);
|
|
87
|
+
if (result.ok) {
|
|
88
|
+
hub.pushEvent("lock_released", { file_path: filePath });
|
|
89
|
+
res.send(JSON.stringify({ deleted: true }));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
sendError(res, 400, JSON.stringify(dbErrorToJson(result.error)));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
/** Register POST /admin/delete-agent route. */
|
|
97
|
+
const registerDeleteAgentRoute = (app, db, hub) => {
|
|
98
|
+
app.post("/admin/delete-agent", (req, res) => {
|
|
99
|
+
const agentName = stringField(req.body, "agentName");
|
|
100
|
+
if (agentName === undefined) {
|
|
101
|
+
sendError(res, 400, "agentName required");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const result = db.adminDeleteAgent(agentName);
|
|
105
|
+
if (result.ok) {
|
|
106
|
+
hub.pushEvent("agent_deleted", { agent_name: agentName });
|
|
107
|
+
res.send(JSON.stringify({ deleted: true }));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
sendError(res, 400, JSON.stringify(dbErrorToJson(result.error)));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
/** Register POST /admin/reset-key route. */
|
|
115
|
+
const registerResetKeyRoute = (app, db) => {
|
|
116
|
+
app.post("/admin/reset-key", (req, res) => {
|
|
117
|
+
const agentName = stringField(req.body, "agentName");
|
|
118
|
+
if (agentName === undefined) {
|
|
119
|
+
sendError(res, 400, "agentName required");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const result = db.adminResetKey(agentName);
|
|
123
|
+
if (result.ok) {
|
|
124
|
+
res.send(JSON.stringify(agentRegistrationToJson(result.value)));
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
sendError(res, 400, JSON.stringify(dbErrorToJson(result.error)));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
/** Register POST /admin/send-message route. */
|
|
132
|
+
const registerSendMessageRoute = (app, db, hub) => {
|
|
133
|
+
app.post("/admin/send-message", (req, res) => {
|
|
134
|
+
const body = req.body;
|
|
135
|
+
const fromAgent = stringField(body, "fromAgent");
|
|
136
|
+
const toAgent = stringField(body, "toAgent");
|
|
137
|
+
const content = stringField(body, "content");
|
|
138
|
+
if (fromAgent === undefined || toAgent === undefined || content === undefined) {
|
|
139
|
+
sendError(res, 400, "fromAgent, toAgent, content required");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const result = db.adminSendMessage(fromAgent, toAgent, content);
|
|
143
|
+
if (result.ok) {
|
|
144
|
+
hub.pushEvent("message_sent", {
|
|
145
|
+
from_agent: fromAgent,
|
|
146
|
+
to_agent: toAgent,
|
|
147
|
+
message_id: result.value,
|
|
148
|
+
});
|
|
149
|
+
res.send(JSON.stringify({ sent: true, message_id: result.value }));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
sendError(res, 400, JSON.stringify(dbErrorToJson(result.error)));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
/** Register POST /admin/reset route. */
|
|
157
|
+
const registerResetRoute = (app, db, hub) => {
|
|
158
|
+
app.post("/admin/reset", (_req, res) => {
|
|
159
|
+
const result = db.adminReset();
|
|
160
|
+
if (result.ok) {
|
|
161
|
+
hub.pushEvent("state_reset", {});
|
|
162
|
+
res.send(JSON.stringify({ reset: true }));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
sendError(res, 500, JSON.stringify(dbErrorToJson(result.error)));
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
/** Register all admin routes on an Express app. */
|
|
170
|
+
export const registerAdminRoutes = (app, db, hub) => {
|
|
171
|
+
app.use(express.json());
|
|
172
|
+
registerStatusRoute(app, db);
|
|
173
|
+
registerDeleteLockRoute(app, db, hub);
|
|
174
|
+
registerDeleteAgentRoute(app, db, hub);
|
|
175
|
+
registerResetKeyRoute(app, db);
|
|
176
|
+
registerSendMessageRoute(app, db, hub);
|
|
177
|
+
registerResetRoute(app, db, hub);
|
|
178
|
+
};
|
|
179
|
+
//# sourceMappingURL=admin_routes.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// Configuration for Too Many Cooks MCP server.
|
|
2
|
+
///
|
|
3
|
+
/// Database path resolution
|
|
4
|
+
/// lives in the data package to guarantee a single source of truth.
|
|
5
|
+
export { createDataConfig, createDataConfigFromWorkspace, defaultConfig, DEFAULT_PORT as defaultPort, getServerPort, getWorkspaceFolder, resolveDbPath, } from "./data/data.js";
|
|
6
|
+
/** Server entry point relative path. */
|
|
7
|
+
export const SERVER_BINARY = "bin/server.ts";
|
|
8
|
+
/** Node args needed to run the server (tsx loader for TypeScript). */
|
|
9
|
+
export const SERVER_NODE_ARGS = ["--import", "tsx"];
|
|
10
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/// Configuration for Too Many Cooks data layer.
|
|
2
|
+
///
|
|
3
|
+
/// SINGLE SOURCE OF TRUTH for database path resolution.
|
|
4
|
+
/// Only the MCP server uses the database. The VSCode extension
|
|
5
|
+
/// communicates exclusively via HTTP to the MCP server.
|
|
6
|
+
/// The database is ALWAYS at `${workspaceFolder}/.too_many_cooks/data.db`.
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
/** Resolve database path for a workspace folder. */
|
|
9
|
+
export const resolveDbPath = (workspaceFolder) => `${workspaceFolder}/.too_many_cooks/data.db`;
|
|
10
|
+
/** Default lock timeout in milliseconds (10 minutes). */
|
|
11
|
+
export const DEFAULT_LOCK_TIMEOUT_MS = 600000;
|
|
12
|
+
/** Default maximum message length in characters. */
|
|
13
|
+
export const DEFAULT_MAX_MESSAGE_LENGTH = 200;
|
|
14
|
+
/** Default maximum plan field length in characters. */
|
|
15
|
+
export const DEFAULT_MAX_PLAN_LENGTH = 100;
|
|
16
|
+
/** Create config with explicit dbPath. */
|
|
17
|
+
export const createDataConfig = (params) => ({
|
|
18
|
+
dbPath: params.dbPath,
|
|
19
|
+
lockTimeoutMs: params.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS,
|
|
20
|
+
maxMessageLength: params.maxMessageLength ?? DEFAULT_MAX_MESSAGE_LENGTH,
|
|
21
|
+
maxPlanLength: params.maxPlanLength ?? DEFAULT_MAX_PLAN_LENGTH,
|
|
22
|
+
});
|
|
23
|
+
/** Create config from workspace folder. */
|
|
24
|
+
export const createDataConfigFromWorkspace = (workspaceFolder) => createDataConfig({ dbPath: resolveDbPath(workspaceFolder) });
|
|
25
|
+
/** Get workspace folder from TMC_WORKSPACE env var or process.cwd(). */
|
|
26
|
+
export const getWorkspaceFolder = () => process.env.TMC_WORKSPACE ?? process.cwd();
|
|
27
|
+
/** Default server port. */
|
|
28
|
+
export const DEFAULT_PORT = 4040;
|
|
29
|
+
/** Get server port from TMC_PORT env var or default (4040). */
|
|
30
|
+
export const getServerPort = () => {
|
|
31
|
+
const raw = process.env.TMC_PORT;
|
|
32
|
+
if (raw === undefined) {
|
|
33
|
+
return DEFAULT_PORT;
|
|
34
|
+
}
|
|
35
|
+
const parsed = parseInt(raw, 10);
|
|
36
|
+
return isNaN(parsed) ? DEFAULT_PORT : parsed;
|
|
37
|
+
};
|
|
38
|
+
/** Default configuration using the resolved workspace folder. */
|
|
39
|
+
export const defaultConfig = createDataConfigFromWorkspace(getWorkspaceFolder());
|
|
40
|
+
/** Join path segments. */
|
|
41
|
+
export const pathJoin = (segments) => path.join(...segments);
|
|
42
|
+
//# sourceMappingURL=config.js.map
|