@maintainer-pro/ai-server 0.1.7 → 0.1.8

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/package.json +3 -3
  2. package/src/server.mjs +122 -48
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainer-pro/ai-server",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Node HTTP server for Maintainer Pro chat. Uses @maintainer-pro/ai-cli to talk to coding agents.",
5
5
  "keywords": [
6
6
  "maintainer-pro",
@@ -30,8 +30,8 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
- "@maintainer-pro/ai-cli": "^0.1.7",
34
- "@maintainer-pro/ai-ui": "^0.2.5",
33
+ "@maintainer-pro/ai-cli": "^0.1.14",
34
+ "@maintainer-pro/ai-ui": "^0.2.8",
35
35
  "ws": "^8.21.3"
36
36
  },
37
37
  "engines": {
package/src/server.mjs CHANGED
@@ -93,6 +93,35 @@ function tryResolveIife(from) {
93
93
  }
94
94
  }
95
95
 
96
+ function readJsonVersion(file) {
97
+ try {
98
+ const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
99
+ return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ function readDepPackageVersion(name) {
106
+ try {
107
+ return readJsonVersion(createRequire(import.meta.url).resolve(`${name}/package.json`));
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ export const PACKAGE_VERSION =
114
+ readJsonVersion(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json")) ||
115
+ "0.0.0";
116
+
117
+ export function runtimePackageVersions() {
118
+ return {
119
+ server: PACKAGE_VERSION,
120
+ cli: readDepPackageVersion("@maintainer-pro/ai-cli"),
121
+ ui: readDepPackageVersion("@maintainer-pro/ai-ui"),
122
+ };
123
+ }
124
+
96
125
  export function findIife() {
97
126
  const here = path.dirname(fileURLToPath(import.meta.url));
98
127
  const cwdPkg = path.join(process.cwd(), "package.json");
@@ -367,11 +396,18 @@ export async function startAiServer(options = {}) {
367
396
  return {
368
397
  status: 200,
369
398
  headers: outHeaders,
370
- body: Buffer.from(JSON.stringify({ ok: true, workspace: workspaceDir })),
399
+ body: Buffer.from(
400
+ JSON.stringify({
401
+ ok: true,
402
+ workspace: workspaceDir,
403
+ versions: runtimePackageVersions(),
404
+ })
405
+ ),
371
406
  };
372
407
  }
373
408
  if (pathname === "/embed-config.js") {
374
409
  const aiServerUrl = resolvePublicUrl({ headers });
410
+ const versions = runtimePackageVersions();
375
411
  const payload = {
376
412
  aiServerUrl,
377
413
  apiUrl: `${aiServerUrl}/api/chat`,
@@ -382,6 +418,7 @@ export async function startAiServer(options = {}) {
382
418
  maintainerProApiKey:
383
419
  pickEnv(env, "MAINTAINER_PRO_CLIENT_API_KEY") ||
384
420
  pickEnv(env, "NEXT_PUBLIC_MAINTAINER_PRO_CLIENT_API_KEY"),
421
+ versions,
385
422
  };
386
423
  logger.debug({ aiServerUrl }, "embed-config");
387
424
  outHeaders["content-type"] = "text/javascript; charset=utf-8";
@@ -484,7 +521,23 @@ export async function startAiServer(options = {}) {
484
521
  return messages;
485
522
  }
486
523
 
487
- async function runChatPost(body) {
524
+ async function runChatPost(body, opts = {}) {
525
+ const userMessageId =
526
+ typeof body.userMessageId === "string" ? body.userMessageId : "";
527
+ const conversationId =
528
+ typeof body.conversationId === "string" ? body.conversationId : "";
529
+ let claimedHere = false;
530
+ if (userMessageId && !opts.alreadyClaimed) {
531
+ if (runningTurns.has(userMessageId)) {
532
+ logger.debug({ userMessageId }, "runChatPost duplicate");
533
+ return { ok: true, status: 200, data: { duplicate: true } };
534
+ }
535
+ runningTurns.add(userMessageId);
536
+ if (conversationId) {
537
+ runningByConversation.set(conversationId, userMessageId);
538
+ }
539
+ claimedHere = true;
540
+ }
488
541
  logger.debug(
489
542
  {
490
543
  conversationId: body.conversationId,
@@ -498,25 +551,37 @@ export async function startAiServer(options = {}) {
498
551
  },
499
552
  "runChatPost"
500
553
  );
501
- const response = await handlers.POST(
502
- new Request("http://127.0.0.1/api/chat", {
503
- method: "POST",
504
- headers: { "content-type": "application/json" },
505
- body: JSON.stringify(body),
506
- })
507
- );
508
- const text = await response.text();
509
- let data = {};
510
554
  try {
511
- data = text ? JSON.parse(text) : {};
512
- } catch {
513
- data = { error: text || "invalid chat response" };
555
+ const response = await handlers.POST(
556
+ new Request("http://127.0.0.1/api/chat", {
557
+ method: "POST",
558
+ headers: { "content-type": "application/json" },
559
+ body: JSON.stringify(body),
560
+ })
561
+ );
562
+ const text = await response.text();
563
+ let data = {};
564
+ try {
565
+ data = text ? JSON.parse(text) : {};
566
+ } catch {
567
+ data = { error: text || "invalid chat response" };
568
+ }
569
+ logger.debug(
570
+ { conversationId: body.conversationId, ok: response.ok, status: response.status },
571
+ "runChatPost done"
572
+ );
573
+ return { ok: response.ok, status: response.status, data };
574
+ } finally {
575
+ if (claimedHere && userMessageId) {
576
+ runningTurns.delete(userMessageId);
577
+ if (
578
+ conversationId &&
579
+ runningByConversation.get(conversationId) === userMessageId
580
+ ) {
581
+ runningByConversation.delete(conversationId);
582
+ }
583
+ }
514
584
  }
515
- logger.debug(
516
- { conversationId: body.conversationId, ok: response.ok, status: response.status },
517
- "runChatPost done"
518
- );
519
- return { ok: response.ok, status: response.status, data };
520
585
  }
521
586
 
522
587
  function publishChatResult(
@@ -607,20 +672,23 @@ export async function startAiServer(options = {}) {
607
672
  userMessageId,
608
673
  });
609
674
  const assistantMessageId = randomUUID();
610
- const result = await runChatPost({
611
- conversationId,
612
- messages,
613
- userMessage: content,
614
- skipPersistUser: true,
615
- userMessageId,
616
- assistantMessageId,
617
- senderType: event.message.senderType === "client" ? "client" : undefined,
618
- senderName: event.message.senderName ?? undefined,
619
- context:
620
- event.message.context && typeof event.message.context === "object"
621
- ? event.message.context
622
- : undefined,
623
- });
675
+ const result = await runChatPost(
676
+ {
677
+ conversationId,
678
+ messages,
679
+ userMessage: content,
680
+ skipPersistUser: true,
681
+ userMessageId,
682
+ assistantMessageId,
683
+ senderType: event.message.senderType === "client" ? "client" : undefined,
684
+ senderName: event.message.senderName ?? undefined,
685
+ context:
686
+ event.message.context && typeof event.message.context === "object"
687
+ ? event.message.context
688
+ : undefined,
689
+ },
690
+ { alreadyClaimed: true }
691
+ );
624
692
  publishChatResult(conversationId, result, assistantMessageId, userMessageId);
625
693
  if (!result.ok) {
626
694
  logger.warn({ status: result.status }, "ai turn failed");
@@ -799,18 +867,21 @@ export async function startAiServer(options = {}) {
799
867
  conversationId,
800
868
  userMessageId,
801
869
  });
802
- const result = await runChatPost({
803
- conversationId,
804
- messages,
805
- userMessage,
806
- skipPersistUser: msg.skipPersistUser !== false,
807
- userMessageId,
808
- assistantMessageId,
809
- senderType: typeof msg.senderType === "string" ? msg.senderType : undefined,
810
- senderName: typeof msg.senderName === "string" ? msg.senderName : undefined,
811
- context:
812
- msg.context && typeof msg.context === "object" ? msg.context : undefined,
813
- });
870
+ const result = await runChatPost(
871
+ {
872
+ conversationId,
873
+ messages,
874
+ userMessage,
875
+ skipPersistUser: msg.skipPersistUser !== false,
876
+ userMessageId,
877
+ assistantMessageId,
878
+ senderType: typeof msg.senderType === "string" ? msg.senderType : undefined,
879
+ senderName: typeof msg.senderName === "string" ? msg.senderName : undefined,
880
+ context:
881
+ msg.context && typeof msg.context === "object" ? msg.context : undefined,
882
+ },
883
+ { alreadyClaimed: true }
884
+ );
814
885
  publishChatResult(conversationId, result, assistantMessageId, userMessageId);
815
886
  if (!result.ok) {
816
887
  throw new Error(result.data?.error ?? `chat failed (${result.status})`);
@@ -893,13 +964,13 @@ export async function startAiServer(options = {}) {
893
964
  (m) =>
894
965
  m.role === "user" &&
895
966
  m.queueStatus === "working" &&
896
- m.senderType !== "developer"
967
+ m.provider !== "developer"
897
968
  );
898
969
  const hasQueued = msgs.some(
899
970
  (m) =>
900
971
  m.role === "user" &&
901
972
  m.queueStatus === "queued" &&
902
- m.senderType !== "developer"
973
+ m.provider !== "developer"
903
974
  );
904
975
  if (!hasWorking && !hasQueued) continue;
905
976
  const result = await db.recoverQueue(conv.id);
@@ -969,7 +1040,10 @@ export async function startAiServer(options = {}) {
969
1040
  /\/$/,
970
1041
  ""
971
1042
  ) || `http://localhost:${port}`;
972
- logger.info(`chat → ${base}/api/chat`);
1043
+ logger.info(
1044
+ { versions: runtimePackageVersions() },
1045
+ `chat → ${base}/api/chat`
1046
+ );
973
1047
  logger.info(`ws → ${toHttpWsUrl(base)}/api/ws`);
974
1048
  logger.info(`ui → ${base}/`);
975
1049
  logger.info(`embed-config → ${base}/embed-config.js`);