@maintainer-pro/ai-server 0.1.6 → 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 +127 -42
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainer-pro/ai-server",
3
- "version": "0.1.6",
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,16 +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
- });
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
+ );
620
692
  publishChatResult(conversationId, result, assistantMessageId, userMessageId);
621
693
  if (!result.ok) {
622
694
  logger.warn({ status: result.status }, "ai turn failed");
@@ -716,6 +788,7 @@ export async function startAiServer(options = {}) {
716
788
  hasMessages: Array.isArray(msg.messages),
717
789
  messageCount: Array.isArray(msg.messages) ? msg.messages.length : 0,
718
790
  skipPersistUser: msg.skipPersistUser,
791
+ hasContext: Boolean(msg.context && typeof msg.context === "object"),
719
792
  },
720
793
  "ws chat.run"
721
794
  );
@@ -736,6 +809,10 @@ export async function startAiServer(options = {}) {
736
809
  typeof msg.senderType === "string" ? msg.senderType : "client",
737
810
  senderName:
738
811
  typeof msg.senderName === "string" ? msg.senderName : null,
812
+ context:
813
+ msg.context && typeof msg.context === "object"
814
+ ? msg.context
815
+ : undefined,
739
816
  },
740
817
  });
741
818
  logger.info(
@@ -790,16 +867,21 @@ export async function startAiServer(options = {}) {
790
867
  conversationId,
791
868
  userMessageId,
792
869
  });
793
- const result = await runChatPost({
794
- conversationId,
795
- messages,
796
- userMessage,
797
- skipPersistUser: msg.skipPersistUser !== false,
798
- userMessageId,
799
- assistantMessageId,
800
- senderType: typeof msg.senderType === "string" ? msg.senderType : undefined,
801
- senderName: typeof msg.senderName === "string" ? msg.senderName : undefined,
802
- });
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
+ );
803
885
  publishChatResult(conversationId, result, assistantMessageId, userMessageId);
804
886
  if (!result.ok) {
805
887
  throw new Error(result.data?.error ?? `chat failed (${result.status})`);
@@ -882,13 +964,13 @@ export async function startAiServer(options = {}) {
882
964
  (m) =>
883
965
  m.role === "user" &&
884
966
  m.queueStatus === "working" &&
885
- m.senderType !== "developer"
967
+ m.provider !== "developer"
886
968
  );
887
969
  const hasQueued = msgs.some(
888
970
  (m) =>
889
971
  m.role === "user" &&
890
972
  m.queueStatus === "queued" &&
891
- m.senderType !== "developer"
973
+ m.provider !== "developer"
892
974
  );
893
975
  if (!hasWorking && !hasQueued) continue;
894
976
  const result = await db.recoverQueue(conv.id);
@@ -958,7 +1040,10 @@ export async function startAiServer(options = {}) {
958
1040
  /\/$/,
959
1041
  ""
960
1042
  ) || `http://localhost:${port}`;
961
- logger.info(`chat → ${base}/api/chat`);
1043
+ logger.info(
1044
+ { versions: runtimePackageVersions() },
1045
+ `chat → ${base}/api/chat`
1046
+ );
962
1047
  logger.info(`ws → ${toHttpWsUrl(base)}/api/ws`);
963
1048
  logger.info(`ui → ${base}/`);
964
1049
  logger.info(`embed-config → ${base}/embed-config.js`);