@csdwd/ai-teams-server 0.2.0 → 0.3.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.
Files changed (2) hide show
  1. package/dist/index.js +269 -44
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/index.ts
4
4
  import os from "node:os";
5
- import path from "node:path";
6
- import fs from "node:fs";
5
+ import path2 from "node:path";
6
+ import fs2 from "node:fs";
7
7
  import { randomUUID as randomUUID2 } from "node:crypto";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import Fastify from "fastify";
@@ -231,6 +231,171 @@ function parseEncryptionKey(hex) {
231
231
  return key;
232
232
  }
233
233
 
234
+ // ../../packages/shared/dist/daemon.js
235
+ import fs from "node:fs";
236
+ import path from "node:path";
237
+ import { spawn } from "node:child_process";
238
+ function readPidFile(pidFile) {
239
+ try {
240
+ const content = fs.readFileSync(pidFile, "utf-8").trim();
241
+ const pid = Number(content);
242
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+ function writePidFile(pidFile, pid) {
248
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
249
+ fs.writeFileSync(pidFile, String(pid), "utf-8");
250
+ }
251
+ function removePidFile(pidFile) {
252
+ try {
253
+ fs.unlinkSync(pidFile);
254
+ } catch {
255
+ }
256
+ }
257
+ function isProcessRunning(pid) {
258
+ try {
259
+ process.kill(pid, 0);
260
+ return true;
261
+ } catch {
262
+ return false;
263
+ }
264
+ }
265
+ function spawnWorker(script, args, logDir) {
266
+ fs.mkdirSync(logDir, { recursive: true });
267
+ const logFile = path.join(logDir, "worker.log");
268
+ const logStream = fs.openSync(logFile, "a");
269
+ const child = spawn(process.execPath, [script, ...args], {
270
+ stdio: ["ignore", logStream, logStream],
271
+ env: {
272
+ ...process.env,
273
+ __AI_TEAMS_DAEMON_WORKER: "1",
274
+ __AI_TEAMS_DAEMON_WATCHDOG: void 0
275
+ }
276
+ });
277
+ fs.closeSync(logStream);
278
+ return child;
279
+ }
280
+ var MAX_RESTARTS_PER_MINUTE = 10;
281
+ var RESTART_DELAY_MS = 3e3;
282
+ async function runWatchdog(opts) {
283
+ const { name, pidFile, logDir, workerScript, workerArgs } = opts;
284
+ writePidFile(pidFile, process.pid);
285
+ let restartTimestamps = [];
286
+ function cleanupAndExit(code) {
287
+ removePidFile(pidFile);
288
+ process.exit(code);
289
+ }
290
+ let worker = null;
291
+ let shuttingDown = false;
292
+ function shutdown() {
293
+ if (shuttingDown)
294
+ return;
295
+ shuttingDown = true;
296
+ if (worker)
297
+ worker.kill("SIGTERM");
298
+ }
299
+ process.on("SIGTERM", shutdown);
300
+ process.on("SIGINT", shutdown);
301
+ while (!shuttingDown) {
302
+ await new Promise((resolve) => {
303
+ worker = spawnWorker(workerScript, workerArgs, logDir);
304
+ worker.on("exit", (code, signal) => {
305
+ worker = null;
306
+ if (shuttingDown) {
307
+ cleanupAndExit(0);
308
+ }
309
+ if (code === 0) {
310
+ console.log(`${name}: worker exited cleanly, daemon stopping.`);
311
+ cleanupAndExit(0);
312
+ }
313
+ const now = Date.now();
314
+ restartTimestamps = restartTimestamps.filter((t) => now - t < 6e4);
315
+ restartTimestamps.push(now);
316
+ if (restartTimestamps.length > MAX_RESTARTS_PER_MINUTE) {
317
+ console.error(`${name}: exceeded ${MAX_RESTARTS_PER_MINUTE} restarts/minute, daemon stopping.`);
318
+ cleanupAndExit(1);
319
+ }
320
+ console.log(`${name}: worker crashed (code=${code}, signal=${signal}), restarting in ${RESTART_DELAY_MS}ms...`);
321
+ setTimeout(resolve, RESTART_DELAY_MS);
322
+ });
323
+ });
324
+ }
325
+ cleanupAndExit(0);
326
+ }
327
+ function getDaemonStatus(pidFile) {
328
+ const pid = readPidFile(pidFile);
329
+ if (pid === null) {
330
+ return { running: false, pid: null };
331
+ }
332
+ if (isProcessRunning(pid)) {
333
+ return { running: true, pid };
334
+ }
335
+ removePidFile(pidFile);
336
+ return { running: false, pid: null };
337
+ }
338
+ async function stopDaemon(pidFile) {
339
+ const pid = readPidFile(pidFile);
340
+ if (pid === null) {
341
+ console.log("Not running (no PID file found).");
342
+ return;
343
+ }
344
+ if (!isProcessRunning(pid)) {
345
+ removePidFile(pidFile);
346
+ console.log("Not running (stale PID file cleaned).");
347
+ return;
348
+ }
349
+ process.kill(pid, "SIGTERM");
350
+ for (let i = 0; i < 20; i++) {
351
+ await new Promise((resolve) => setTimeout(resolve, 500));
352
+ if (!isProcessRunning(pid)) {
353
+ removePidFile(pidFile);
354
+ console.log(`Stopped (PID ${pid}).`);
355
+ return;
356
+ }
357
+ }
358
+ process.kill(pid, "SIGKILL");
359
+ removePidFile(pidFile);
360
+ console.log(`Force killed (PID ${pid}).`);
361
+ }
362
+ function daemonize(opts) {
363
+ if (process.env.__AI_TEAMS_DAEMON_WORKER === "1") {
364
+ return opts.run();
365
+ }
366
+ if (process.env.__AI_TEAMS_DAEMON_WATCHDOG === "1") {
367
+ return runWatchdog({
368
+ name: opts.name,
369
+ pidFile: opts.pidFile,
370
+ logDir: path.dirname(opts.logFile),
371
+ workerScript: process.argv[1],
372
+ workerArgs: process.argv.slice(2)
373
+ });
374
+ }
375
+ fs.mkdirSync(path.dirname(opts.logFile), { recursive: true });
376
+ const child = spawn(process.execPath, [process.argv[1], ...process.argv.slice(2)], {
377
+ detached: true,
378
+ stdio: ["ignore", "ignore", "ignore"],
379
+ env: {
380
+ ...process.env,
381
+ __AI_TEAMS_DAEMON_WATCHDOG: "1"
382
+ }
383
+ });
384
+ child.unref();
385
+ return new Promise((resolve) => {
386
+ setTimeout(() => {
387
+ const pid = readPidFile(opts.pidFile);
388
+ if (pid) {
389
+ console.log(`${opts.name} started (PID ${pid})`);
390
+ } else {
391
+ console.error(`${opts.name}: failed to start \u2014 PID file not found.`);
392
+ }
393
+ resolve();
394
+ process.exit(pid ? 0 : 1);
395
+ }, 500);
396
+ });
397
+ }
398
+
234
399
  // src/db.ts
235
400
  import { DatabaseSync } from "node:sqlite";
236
401
  import pg from "pg";
@@ -1919,8 +2084,8 @@ async function createAiTeamsServer(options) {
1919
2084
  const defaultTimeoutSec = options.defaultTimeoutSec ?? 1800;
1920
2085
  const disconnectGraceMs = options.disconnectGraceMs ?? 15e3;
1921
2086
  const maxLogChunksPerTask = options.maxLogChunksPerTask ?? 400;
1922
- const dataDir = options.dataDir ?? path.join(process.cwd(), "data");
1923
- const dbPath = options.dbPath ?? path.join(dataDir, "ai-teams.db");
2087
+ const dataDir = options.dataDir ?? path2.join(process.cwd(), "data");
2088
+ const dbPath = options.dbPath ?? path2.join(dataDir, "ai-teams.db");
1924
2089
  let closing = false;
1925
2090
  const state = createInMemoryStateStore();
1926
2091
  const db = await createDatabaseFromEnv({
@@ -1935,13 +2100,13 @@ async function createAiTeamsServer(options) {
1935
2100
  const logDir = options.logDir || process.env.LOG_DIR;
1936
2101
  let loggerConfig = { level: logLevel };
1937
2102
  if (logDir && options.logger !== false) {
1938
- fs.mkdirSync(logDir, { recursive: true });
2103
+ fs2.mkdirSync(logDir, { recursive: true });
1939
2104
  loggerConfig = {
1940
2105
  level: logLevel,
1941
2106
  transport: {
1942
2107
  targets: [
1943
2108
  { target: "pino/file", options: { destination: 1 }, level: logLevel },
1944
- { target: "pino/file", options: { destination: path.join(logDir, "server.log") }, level: logLevel }
2109
+ { target: "pino/file", options: { destination: path2.join(logDir, "server.log") }, level: logLevel }
1945
2110
  ]
1946
2111
  }
1947
2112
  };
@@ -1974,8 +2139,8 @@ async function createAiTeamsServer(options) {
1974
2139
  },
1975
2140
  staticCSP: true
1976
2141
  });
1977
- const webDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "web");
1978
- if (fs.existsSync(webDir)) {
2142
+ const webDir = path2.join(path2.dirname(fileURLToPath(import.meta.url)), "web");
2143
+ if (fs2.existsSync(webDir)) {
1979
2144
  await app.register(fastifyStatic, { root: webDir, prefix: "/" });
1980
2145
  app.setNotFoundHandler((_, reply) => {
1981
2146
  reply.sendFile("index.html");
@@ -2086,19 +2251,19 @@ async function createAiTeamsServer(options) {
2086
2251
  return { employeeId, workspace: null, activeSessionId: null, sessions: [] };
2087
2252
  }
2088
2253
  const encodedPath = workspace.replace(/\//g, "-");
2089
- const claudeProjectsDir = path.join(os.homedir(), ".claude", "projects", encodedPath);
2254
+ const claudeProjectsDir = path2.join(os.homedir(), ".claude", "projects", encodedPath);
2090
2255
  let entries;
2091
2256
  try {
2092
- entries = fs.readdirSync(claudeProjectsDir, { withFileTypes: true });
2257
+ entries = fs2.readdirSync(claudeProjectsDir, { withFileTypes: true });
2093
2258
  } catch {
2094
2259
  return { employeeId, workspace, activeSessionId: null, sessions: [] };
2095
2260
  }
2096
2261
  const jsonlFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).sort((a, b) => b.name.localeCompare(a.name));
2097
2262
  const sessions = jsonlFiles.map((entry) => {
2098
- const filePath = path.join(claudeProjectsDir, entry.name);
2099
- const stat = fs.statSync(filePath);
2263
+ const filePath = path2.join(claudeProjectsDir, entry.name);
2264
+ const stat = fs2.statSync(filePath);
2100
2265
  const sessionId = entry.name.replace(/\.jsonl$/, "");
2101
- const content = fs.readFileSync(filePath, "utf8");
2266
+ const content = fs2.readFileSync(filePath, "utf8");
2102
2267
  const lines = content.split("\n").filter(Boolean);
2103
2268
  const lineCount = lines.length;
2104
2269
  let firstUserMessage = null;
@@ -2111,9 +2276,9 @@ async function createAiTeamsServer(options) {
2111
2276
  return { id: sessionId, sizeBytes: stat.size, modifiedAt: stat.mtime.toISOString(), lineCount, firstUserMessage, latestUserMessage };
2112
2277
  });
2113
2278
  let activeSessionId = null;
2114
- const agentStatePath = path.join(workspace, ".ai-teams", "agents", employeeId, "session-state.json");
2279
+ const agentStatePath = path2.join(workspace, ".ai-teams", "agents", employeeId, "session-state.json");
2115
2280
  try {
2116
- const raw = fs.readFileSync(agentStatePath, "utf8");
2281
+ const raw = fs2.readFileSync(agentStatePath, "utf8");
2117
2282
  activeSessionId = JSON.parse(raw).claudeSessionId ?? null;
2118
2283
  } catch {
2119
2284
  }
@@ -2124,7 +2289,7 @@ async function createAiTeamsServer(options) {
2124
2289
  if (request.url.startsWith("/ws/") || request.url.startsWith("/docs")) {
2125
2290
  return;
2126
2291
  }
2127
- const ext = path.extname(request.url.split("?")[0]);
2292
+ const ext = path2.extname(request.url.split("?")[0]);
2128
2293
  if (request.url === "/" || staticExts.has(ext)) {
2129
2294
  return;
2130
2295
  }
@@ -2693,23 +2858,52 @@ async function startServer(options = readOptionsFromEnv()) {
2693
2858
  await server.app.listen({ port: options.port ?? DEFAULT_PORT, host: options.host ?? "0.0.0.0" });
2694
2859
  return server;
2695
2860
  }
2696
- var isCli = process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
2861
+ var isCli = process.argv[1] && fs2.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
2697
2862
  if (isCli) {
2698
2863
  let getArgValue = function(name) {
2699
2864
  const idx = args.indexOf(name);
2700
2865
  if (idx === -1) return void 0;
2701
2866
  return args[idx + 1];
2867
+ }, resolveDataDir = function() {
2868
+ return getArgValue("--data-dir") || process.env.DATA_DIR || path2.join(process.cwd(), "data");
2869
+ }, resolvePidFile = function() {
2870
+ return path2.join(resolveDataDir(), ".ai-teams-server.pid");
2871
+ }, resolveLogDir = function() {
2872
+ return getArgValue("--log-dir") || process.env.LOG_DIR || path2.join(resolveDataDir(), "logs");
2873
+ }, applyCliArgsToEnv = function() {
2874
+ const cliToken = getArgValue("--token");
2875
+ const cliPort = getArgValue("--port");
2876
+ const cliHost = getArgValue("--host");
2877
+ const cliDataDir = getArgValue("--data-dir");
2878
+ const cliDatabaseUrl = getArgValue("--database-url");
2879
+ const cliDbPath = getArgValue("--db-path");
2880
+ const cliLogLevel = getArgValue("--log-level");
2881
+ const cliLogDir = getArgValue("--log-dir");
2882
+ if (cliToken) process.env.AI_TEAMS_AUTH_TOKEN = cliToken;
2883
+ if (cliPort) process.env.AI_TEAMS_SERVER_PORT = cliPort;
2884
+ if (cliHost) process.env.HOST = cliHost;
2885
+ if (cliDataDir) process.env.DATA_DIR = cliDataDir;
2886
+ if (cliDatabaseUrl) process.env.DATABASE_URL = cliDatabaseUrl;
2887
+ if (cliDbPath) process.env.DB_PATH = cliDbPath;
2888
+ if (cliLogLevel) process.env.LOG_LEVEL = cliLogLevel;
2889
+ if (cliLogDir) process.env.LOG_DIR = cliLogDir;
2702
2890
  };
2703
- getArgValue2 = getArgValue;
2891
+ getArgValue2 = getArgValue, resolveDataDir2 = resolveDataDir, resolvePidFile2 = resolvePidFile, resolveLogDir2 = resolveLogDir, applyCliArgsToEnv2 = applyCliArgsToEnv;
2704
2892
  const args = process.argv.slice(2);
2705
2893
  if (args.includes("--version") || args.includes("-v")) {
2706
- console.log("0.2.0");
2894
+ console.log("0.3.0");
2707
2895
  process.exit(0);
2708
2896
  }
2709
2897
  if (args.includes("--help") || args.includes("-h")) {
2710
2898
  console.log(`ai-teams-server \u2014 AI Teams \u4E2D\u592E\u670D\u52A1\u5668
2711
2899
 
2712
- \u7528\u6CD5: ai-teams-server [\u9009\u9879]
2900
+ \u7528\u6CD5: ai-teams-server <command> [\u9009\u9879]
2901
+
2902
+ \u547D\u4EE4:
2903
+ start [\u9009\u9879] \u540E\u53F0\u542F\u52A8\u5B88\u62A4\u8FDB\u7A0B
2904
+ stop \u505C\u6B62\u5B88\u62A4\u8FDB\u7A0B
2905
+ restart [\u9009\u9879] \u91CD\u542F\u5B88\u62A4\u8FDB\u7A0B
2906
+ status \u67E5\u770B\u8FD0\u884C\u72B6\u6001
2713
2907
 
2714
2908
  \u9009\u9879:
2715
2909
  --token <token> \u8BA4\u8BC1 Token (\u5FC5\u586B\uFF0C\u6216\u8BBE AI_TEAMS_AUTH_TOKEN)
@@ -2722,36 +2916,67 @@ if (isCli) {
2722
2916
  --log-dir <dir> \u65E5\u5FD7\u6587\u4EF6\u76EE\u5F55 (\u4E0D\u8BBE\u5219\u4EC5\u8F93\u51FA\u5230 stdout)
2723
2917
  -v, --version \u663E\u793A\u7248\u672C\u53F7
2724
2918
  -h, --help \u663E\u793A\u5E2E\u52A9
2919
+
2920
+ \u4E0D\u5E26\u547D\u4EE4\u76F4\u63A5\u8FD0\u884C\u65F6\u4E3A\u524D\u53F0\u6A21\u5F0F\u3002
2725
2921
  `);
2726
2922
  process.exit(0);
2727
2923
  }
2728
- const cliToken = getArgValue("--token");
2729
- const cliPort = getArgValue("--port");
2730
- const cliHost = getArgValue("--host");
2731
- const cliDataDir = getArgValue("--data-dir");
2732
- const cliDatabaseUrl = getArgValue("--database-url");
2733
- const cliDbPath = getArgValue("--db-path");
2734
- const cliLogLevel = getArgValue("--log-level");
2735
- const cliLogDir = getArgValue("--log-dir");
2736
- if (cliToken) process.env.AI_TEAMS_AUTH_TOKEN = cliToken;
2737
- if (cliPort) process.env.AI_TEAMS_SERVER_PORT = cliPort;
2738
- if (cliHost) process.env.HOST = cliHost;
2739
- if (cliDataDir) process.env.DATA_DIR = cliDataDir;
2740
- if (cliDatabaseUrl) process.env.DATABASE_URL = cliDatabaseUrl;
2741
- if (cliDbPath) process.env.DB_PATH = cliDbPath;
2742
- if (cliLogLevel) process.env.LOG_LEVEL = cliLogLevel;
2743
- if (cliLogDir) process.env.LOG_DIR = cliLogDir;
2744
- const options = readOptionsFromEnv();
2745
- if (!options.authToken) {
2746
- console.error("\u9519\u8BEF: \u9700\u8981\u8BA4\u8BC1 Token\u3002\u4F7F\u7528 --token <token> \u6216\u8BBE\u7F6E AI_TEAMS_AUTH_TOKEN \u73AF\u5883\u53D8\u91CF\u3002");
2747
- process.exit(1);
2924
+ const subcommand = args[0];
2925
+ if (subcommand === "start" || subcommand === "restart") {
2926
+ void (async () => {
2927
+ if (subcommand === "restart") {
2928
+ const status = getDaemonStatus(resolvePidFile());
2929
+ if (status.running) {
2930
+ await stopDaemon(resolvePidFile());
2931
+ }
2932
+ }
2933
+ applyCliArgsToEnv();
2934
+ if (!process.env.AI_TEAMS_AUTH_TOKEN) {
2935
+ console.error("\u9519\u8BEF: \u9700\u8981\u8BA4\u8BC1 Token\u3002\u4F7F\u7528 --token <token> \u6216\u8BBE\u7F6E AI_TEAMS_AUTH_TOKEN \u73AF\u5883\u53D8\u91CF\u3002");
2936
+ process.exit(1);
2937
+ }
2938
+ if (!process.env.LOG_DIR) {
2939
+ process.env.LOG_DIR = resolveLogDir();
2940
+ }
2941
+ await daemonize({
2942
+ name: "ai-teams-server",
2943
+ pidFile: resolvePidFile(),
2944
+ logFile: path2.join(resolveLogDir(), "server.log"),
2945
+ run: async () => {
2946
+ await startServer(readOptionsFromEnv());
2947
+ }
2948
+ });
2949
+ })();
2950
+ } else if (subcommand === "stop") {
2951
+ void (async () => {
2952
+ await stopDaemon(resolvePidFile());
2953
+ })();
2954
+ } else if (subcommand === "status") {
2955
+ const status = getDaemonStatus(resolvePidFile());
2956
+ if (status.running) {
2957
+ console.log(`ai-teams-server is running (PID ${status.pid})`);
2958
+ console.log(`Log: ${path2.join(resolveLogDir(), "server.log")}`);
2959
+ } else {
2960
+ console.log("ai-teams-server is not running.");
2961
+ }
2962
+ } else {
2963
+ applyCliArgsToEnv();
2964
+ const options = readOptionsFromEnv();
2965
+ if (!options.authToken) {
2966
+ console.error("\u9519\u8BEF: \u9700\u8981\u8BA4\u8BC1 Token\u3002\u4F7F\u7528 --token <token> \u6216\u8BBE\u7F6E AI_TEAMS_AUTH_TOKEN \u73AF\u5883\u53D8\u91CF\u3002");
2967
+ process.exit(1);
2968
+ }
2969
+ startServer(options).catch((error) => {
2970
+ console.error(error);
2971
+ process.exit(1);
2972
+ });
2748
2973
  }
2749
- startServer(options).catch((error) => {
2750
- console.error(error);
2751
- process.exit(1);
2752
- });
2753
2974
  }
2754
2975
  var getArgValue2;
2976
+ var resolveDataDir2;
2977
+ var resolvePidFile2;
2978
+ var resolveLogDir2;
2979
+ var applyCliArgsToEnv2;
2755
2980
  export {
2756
2981
  createAiTeamsServer,
2757
2982
  readOptionsFromEnv,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@csdwd/ai-teams-server",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "AI Teams central server — Fastify HTTP + WebSocket server for task dispatch, employee management, and web UI",
5
5
  "type": "module",
6
6
  "bin": {