@jskit-ai/assistant 0.1.31 → 0.1.34

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 (76) hide show
  1. package/package.descriptor.mjs +346 -142
  2. package/package.json +3 -19
  3. package/src/server/buildTemplateContext.js +107 -0
  4. package/templates/migrations/assistant_config_initial.cjs +25 -0
  5. package/templates/migrations/assistant_transcripts_initial.cjs +21 -14
  6. package/templates/src/local-package/client/components/AssistantSettingsClientElement.vue +88 -0
  7. package/templates/src/local-package/client/components/AssistantSurfaceClientElement.vue +10 -0
  8. package/{src/client/composables/useAssistantWorkspaceRuntime.js → templates/src/local-package/client/composables/useAssistantRuntime.js} +91 -114
  9. package/templates/src/local-package/client/index.js +3 -0
  10. package/templates/src/local-package/client/providers/AssistantClientProvider.js +16 -0
  11. package/templates/src/local-package/package.descriptor.mjs +85 -0
  12. package/templates/src/local-package/package.json +11 -0
  13. package/{src/server/AssistantServiceProvider.js → templates/src/local-package/server/AssistantProvider.js} +37 -61
  14. package/templates/src/local-package/server/actionIds.js +9 -0
  15. package/templates/src/local-package/server/actions.js +190 -0
  16. package/templates/src/local-package/server/registerRoutes.js +296 -0
  17. package/templates/src/local-package/server/repositories/assistantConfigRepository.js +141 -0
  18. package/{src → templates/src/local-package}/server/repositories/conversationsRepository.js +44 -45
  19. package/{src → templates/src/local-package}/server/repositories/messagesRepository.js +49 -34
  20. package/templates/src/local-package/server/services/assistantConfigService.js +90 -0
  21. package/{src → templates/src/local-package}/server/services/chatService.js +45 -37
  22. package/{src → templates/src/local-package}/server/services/transcriptService.js +61 -82
  23. package/templates/src/local-package/shared/assistantRuntimeConfig.js +13 -0
  24. package/templates/src/local-package/shared/index.js +1 -0
  25. package/templates/src/pages/assistant/index.vue +7 -0
  26. package/test/buildTemplateContext.test.js +112 -0
  27. package/test/packageDescriptor.test.js +69 -0
  28. package/src/client/components/AssistantClientElement.vue +0 -1316
  29. package/src/client/components/AssistantConsoleSettingsClientElement.vue +0 -70
  30. package/src/client/components/AssistantSettingsFormCard.vue +0 -76
  31. package/src/client/components/AssistantWorkspaceClientElement.vue +0 -15
  32. package/src/client/components/AssistantWorkspaceSettingsClientElement.vue +0 -72
  33. package/src/client/index.js +0 -10
  34. package/src/client/lib/assistantApi.js +0 -137
  35. package/src/client/lib/assistantHttpClient.js +0 -10
  36. package/src/client/lib/markdownRenderer.js +0 -31
  37. package/src/client/providers/AssistantWebClientProvider.js +0 -20
  38. package/src/server/actionIds.js +0 -11
  39. package/src/server/actions.js +0 -191
  40. package/src/server/lib/aiClient.js +0 -43
  41. package/src/server/lib/ndjson.js +0 -47
  42. package/src/server/lib/providers/anthropicClient.js +0 -375
  43. package/src/server/lib/providers/common.js +0 -150
  44. package/src/server/lib/providers/deepSeekClient.js +0 -22
  45. package/src/server/lib/providers/openAiClient.js +0 -13
  46. package/src/server/lib/providers/openAiCompatibleClient.js +0 -69
  47. package/src/server/lib/resolveWorkspaceSlug.js +0 -24
  48. package/src/server/lib/serviceToolCatalog.js +0 -459
  49. package/src/server/registerRoutes.js +0 -383
  50. package/src/server/repositories/assistantSettingsRepository.js +0 -100
  51. package/src/server/repositories/repositoryPersistenceUtils.js +0 -48
  52. package/src/server/services/assistantSettingsService.js +0 -149
  53. package/src/shared/assistantPaths.js +0 -50
  54. package/src/shared/assistantResource.js +0 -317
  55. package/src/shared/assistantSettingsResource.js +0 -197
  56. package/src/shared/index.js +0 -43
  57. package/src/shared/queryKeys.js +0 -69
  58. package/src/shared/settingsEvents.js +0 -6
  59. package/src/shared/streamEvents.js +0 -29
  60. package/src/shared/support/conversationStatus.js +0 -18
  61. package/src/shared/support/jsonObject.js +0 -18
  62. package/src/shared/support/positiveInteger.js +0 -9
  63. package/templates/migrations/assistant_settings_initial.cjs +0 -37
  64. package/templates/src/pages/admin/workspace/assistant/index.vue +0 -7
  65. package/test/aiConfigValidation.test.js +0 -15
  66. package/test/assistantApiSurfaceHeader.test.js +0 -64
  67. package/test/assistantResource.test.js +0 -53
  68. package/test/assistantSettingsResource.test.js +0 -48
  69. package/test/assistantSettingsService.test.js +0 -133
  70. package/test/chatService.test.js +0 -841
  71. package/test/descriptorSurfaceOption.test.js +0 -35
  72. package/test/queryKeys.test.js +0 -41
  73. package/test/resolveWorkspaceSlug.test.js +0 -83
  74. package/test/routeInputContracts.test.js +0 -286
  75. package/test/serviceToolCatalog.test.js +0 -1235
  76. package/test/transcriptService.test.js +0 -175
@@ -1,8 +1,11 @@
1
1
  import { AppError, parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
2
2
  import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
- import { ASSISTANT_STREAM_EVENT_TYPES } from "../../shared/streamEvents.js";
4
- import { mapStreamError } from "../lib/ndjson.js";
5
- import { resolveWorkspaceSlug } from "../lib/resolveWorkspaceSlug.js";
3
+ import { resolveWorkspace } from "@jskit-ai/users-core/server/support/resolveWorkspace";
4
+ import { resolveWorkspaceSlug } from "@jskit-ai/assistant-core/server";
5
+ import {
6
+ ASSISTANT_STREAM_EVENT_TYPES
7
+ } from "@jskit-ai/assistant-core/shared";
8
+ import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
6
9
 
7
10
  const MAX_HISTORY_MESSAGES = 20;
8
11
  const MAX_INPUT_CHARS = 8000;
@@ -86,12 +89,7 @@ function isAbortError(error) {
86
89
  return false;
87
90
  }
88
91
 
89
- const errorName = String(error.name || "").trim();
90
- if (errorName === "AbortError") {
91
- return true;
92
- }
93
-
94
- return false;
92
+ return String(error.name || "").trim() === "AbortError";
95
93
  }
96
94
 
97
95
  function extractTextDelta(deltaContent) {
@@ -148,11 +146,11 @@ function buildSystemPrompt({ toolDescriptors = [], workspaceSlug = "", customSys
148
146
  const normalizedWorkspaceSlug = normalizeText(workspaceSlug).toLowerCase();
149
147
  const workspaceLine = normalizedWorkspaceSlug
150
148
  ? `Current workspace slug: ${normalizedWorkspaceSlug}.`
151
- : "Current workspace slug is unavailable.";
149
+ : "No workspace context is active for this assistant.";
152
150
  const normalizedCustomSystemPrompt = String(customSystemPrompt || "").trim();
153
151
 
154
152
  const promptSegments = [
155
- "You are the workspace assistant.",
153
+ `You are the assistant for the ${assistantRuntimeConfig.runtimeSurfaceId} surface.`,
156
154
  "Use tools when they are necessary and only when available.",
157
155
  "Do not mention tools that are not available.",
158
156
  "When answering schema questions, rely only on tool contracts and tool results.",
@@ -534,9 +532,9 @@ function mergeAssistantMessageText(streamedText = "", completionText = "") {
534
532
  return `${streamed}\n${completion}`;
535
533
  }
536
534
 
537
- function createChatService({ aiClient, transcriptService, serviceToolCatalog, assistantSettingsService } = {}) {
538
- if (!aiClient || !transcriptService || !serviceToolCatalog || !assistantSettingsService) {
539
- throw new Error("createChatService requires aiClient, transcriptService, serviceToolCatalog, and assistantSettingsService.");
535
+ function createChatService({ aiClient, transcriptService, serviceToolCatalog, assistantConfigService } = {}) {
536
+ if (!aiClient || !transcriptService || !serviceToolCatalog || !assistantConfigService) {
537
+ throw new Error("createChatService requires aiClient, transcriptService, serviceToolCatalog, and assistantConfigService.");
540
538
  }
541
539
 
542
540
  async function streamChat(payload = {}, options = {}) {
@@ -544,15 +542,15 @@ function createChatService({ aiClient, transcriptService, serviceToolCatalog, as
544
542
  throw new AppError(503, "Assistant provider is not configured.");
545
543
  }
546
544
 
547
- const source = normalizeStreamInput(payload);
548
545
  const context = normalizeObject(options.context);
546
+ const workspace = assistantRuntimeConfig.runtimeSurfaceRequiresWorkspace ? resolveWorkspace(context, payload) : null;
547
+ const source = normalizeStreamInput(payload);
549
548
  const streamWriter = options.streamWriter;
550
549
  if (!hasStreamWriter(streamWriter)) {
551
550
  throw new Error("assistant.chat.stream requires streamWriter methods.");
552
551
  }
553
552
 
554
553
  const actor = context.actor;
555
- const workspace = context.workspace;
556
554
 
557
555
  const conversationResult = await transcriptService.createConversationForTurn(
558
556
  workspace,
@@ -561,7 +559,7 @@ function createChatService({ aiClient, transcriptService, serviceToolCatalog, as
561
559
  conversationId: source.conversationId,
562
560
  provider: aiClient.provider,
563
561
  model: aiClient.defaultModel,
564
- surfaceSid: context.surface,
562
+ surfaceId: assistantRuntimeConfig.runtimeSurfaceId,
565
563
  messageId: source.messageId
566
564
  },
567
565
  {
@@ -583,27 +581,29 @@ function createChatService({ aiClient, transcriptService, serviceToolCatalog, as
583
581
  clientMessageSid: source.messageId,
584
582
  contentText: source.input,
585
583
  metadata: {
586
- surfaceSid: normalizeText(context.surface)
584
+ surfaceId: assistantRuntimeConfig.runtimeSurfaceId
587
585
  }
588
586
  },
589
587
  {
590
- context
588
+ context,
589
+ workspace
591
590
  }
592
591
  );
593
592
 
594
593
  const toolSet = serviceToolCatalog.resolveToolSet(context);
595
- const customSystemPrompt = await assistantSettingsService.resolveSystemPrompt(
594
+ const customSystemPrompt = await assistantConfigService.resolveSystemPrompt(
596
595
  workspace,
597
596
  {
598
- surface: context.surface
597
+ surface: assistantRuntimeConfig.runtimeSurfaceId
599
598
  },
600
599
  {
601
- context
600
+ context,
601
+ input: payload
602
602
  }
603
603
  );
604
604
  const systemPrompt = buildSystemPrompt({
605
605
  toolDescriptors: toolSet.tools,
606
- workspaceSlug: resolveWorkspaceSlug(context, source),
606
+ workspaceSlug: resolveWorkspaceSlug(context, payload),
607
607
  customSystemPrompt
608
608
  });
609
609
 
@@ -634,7 +634,8 @@ function createChatService({ aiClient, transcriptService, serviceToolCatalog, as
634
634
  contentText: normalizedAssistantMessageText
635
635
  },
636
636
  {
637
- context
637
+ context,
638
+ workspace
638
639
  }
639
640
  );
640
641
 
@@ -645,7 +646,8 @@ function createChatService({ aiClient, transcriptService, serviceToolCatalog, as
645
646
  metadata
646
647
  },
647
648
  {
648
- context
649
+ context,
650
+ workspace
649
651
  }
650
652
  );
651
653
 
@@ -689,7 +691,8 @@ function createChatService({ aiClient, transcriptService, serviceToolCatalog, as
689
691
  }
690
692
  },
691
693
  {
692
- context
694
+ context,
695
+ workspace
693
696
  }
694
697
  );
695
698
 
@@ -713,7 +716,8 @@ function createChatService({ aiClient, transcriptService, serviceToolCatalog, as
713
716
  }
714
717
  },
715
718
  {
716
- context
719
+ context,
720
+ workspace
717
721
  }
718
722
  );
719
723
 
@@ -923,7 +927,6 @@ function createChatService({ aiClient, transcriptService, serviceToolCatalog, as
923
927
  } catch (error) {
924
928
  const aborted = isAbortError(error);
925
929
  const status = aborted ? "aborted" : "failed";
926
- const streamError = mapStreamError(error);
927
930
 
928
931
  if (streamed) {
929
932
  await transcriptService.completeConversation(
@@ -932,16 +935,17 @@ function createChatService({ aiClient, transcriptService, serviceToolCatalog, as
932
935
  status
933
936
  },
934
937
  {
935
- context
938
+ context,
939
+ workspace
936
940
  }
937
941
  );
938
942
 
939
943
  streamWriter.sendError({
940
944
  type: ASSISTANT_STREAM_EVENT_TYPES.ERROR,
941
945
  messageId: source.messageId,
942
- code: aborted ? "assistant_stream_aborted" : streamError.code,
943
- message: aborted ? "Assistant request was cancelled." : streamError.message,
944
- status: aborted ? 499 : streamError.status
946
+ code: aborted ? "assistant_stream_aborted" : String(error?.code || "assistant_stream_failed"),
947
+ message: aborted ? "Assistant request was cancelled." : String(error?.message || "Assistant request failed."),
948
+ status: aborted ? 499 : Number(error?.status || error?.statusCode || 500)
945
949
  });
946
950
 
947
951
  streamWriter.sendDone({
@@ -963,14 +967,20 @@ function createChatService({ aiClient, transcriptService, serviceToolCatalog, as
963
967
 
964
968
  async function listConversations(query = {}, options = {}) {
965
969
  const context = normalizeObject(options.context);
966
- return transcriptService.listConversationsForUser(context.workspace, context.actor, query, {
970
+ const workspace = assistantRuntimeConfig.runtimeSurfaceRequiresWorkspace
971
+ ? resolveWorkspace(context, options.input || {})
972
+ : null;
973
+ return transcriptService.listConversationsForUser(workspace, context.actor, query, {
967
974
  context
968
975
  });
969
976
  }
970
977
 
971
978
  async function getConversationMessages(conversationId, query = {}, options = {}) {
972
979
  const context = normalizeObject(options.context);
973
- return transcriptService.getConversationMessagesForUser(context.workspace, context.actor, conversationId, query, {
980
+ const workspace = assistantRuntimeConfig.runtimeSurfaceRequiresWorkspace
981
+ ? resolveWorkspace(context, options.input || {})
982
+ : null;
983
+ return transcriptService.getConversationMessagesForUser(workspace, context.actor, conversationId, query, {
974
984
  context
975
985
  });
976
986
  }
@@ -982,6 +992,4 @@ function createChatService({ aiClient, transcriptService, serviceToolCatalog, as
982
992
  });
983
993
  }
984
994
 
985
- export {
986
- createChatService
987
- };
995
+ export { createChatService };
@@ -1,6 +1,7 @@
1
1
  import { AppError, parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
2
2
  import { normalizeObject, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
- import { normalizeConversationStatus } from "../../shared/support/conversationStatus.js";
3
+ import { normalizeConversationStatus } from "@jskit-ai/assistant-core/shared";
4
+ import { assistantRuntimeConfig } from "../../shared/assistantRuntimeConfig.js";
4
5
 
5
6
  const DEFAULT_PAGE_SIZE = 20;
6
7
  const MAX_PAGE_SIZE = 200;
@@ -9,64 +10,13 @@ const MAX_MESSAGES_PAGE_SIZE = 500;
9
10
  const DEFAULT_CONVERSATION_TITLE = "New conversation";
10
11
  const MAX_CONVERSATION_TITLE_LENGTH = 80;
11
12
 
12
- const serviceEvents = Object.freeze({
13
- createConversationForTurn: Object.freeze([
14
- Object.freeze({
15
- type: "entity.changed",
16
- source: "assistant",
17
- entity: "conversation",
18
- operation: ({ result }) => (result?.created === true ? "created" : "updated"),
19
- entityId: ({ result }) => result?.conversation?.id,
20
- realtime: Object.freeze({
21
- event: "assistant.transcript.changed",
22
- audience: "event_scope",
23
- payload: ({ result }) => ({
24
- conversationId: result?.conversation?.id || null
25
- })
26
- })
27
- })
28
- ]),
29
- appendMessage: Object.freeze([
30
- Object.freeze({
31
- type: "entity.changed",
32
- source: "assistant",
33
- entity: "conversation",
34
- operation: "updated",
35
- entityId: ({ result }) => result?.conversationId,
36
- realtime: Object.freeze({
37
- event: "assistant.transcript.changed",
38
- audience: "event_scope",
39
- payload: ({ result }) => ({
40
- conversationId: result?.conversationId || null
41
- })
42
- })
43
- })
44
- ]),
45
- completeConversation: Object.freeze([
46
- Object.freeze({
47
- type: "entity.changed",
48
- source: "assistant",
49
- entity: "conversation",
50
- operation: "updated",
51
- entityId: ({ result }) => result?.id,
52
- realtime: Object.freeze({
53
- event: "assistant.transcript.changed",
54
- audience: "event_scope",
55
- payload: ({ result }) => ({
56
- conversationId: result?.id || null
57
- })
58
- })
59
- })
60
- ])
61
- });
62
-
63
- function resolveWorkspaceId(workspace) {
13
+ function resolveWorkspaceId(workspace, { required = false } = {}) {
64
14
  const workspaceId = parsePositiveInteger(workspace?.id || workspace);
65
- if (!workspaceId) {
15
+ if (!workspaceId && required) {
66
16
  throw new AppError(409, "Workspace selection required.");
67
17
  }
68
18
 
69
- return workspaceId;
19
+ return workspaceId || null;
70
20
  }
71
21
 
72
22
  function resolveActorUserId(user, { required = false } = {}) {
@@ -75,7 +25,7 @@ function resolveActorUserId(user, { required = false } = {}) {
75
25
  throw new AppError(401, "Authentication required.");
76
26
  }
77
27
 
78
- return actorUserId;
28
+ return actorUserId || null;
79
29
  }
80
30
 
81
31
  function normalizePagination(pagination = {}, { defaultPageSize = DEFAULT_PAGE_SIZE, maxPageSize = MAX_PAGE_SIZE } = {}) {
@@ -116,8 +66,14 @@ function createTranscriptService({ conversationsRepository, messagesRepository }
116
66
  throw new Error("createTranscriptService requires conversationsRepository and messagesRepository.");
117
67
  }
118
68
 
69
+ function resolveExpectedWorkspaceId(workspace) {
70
+ return resolveWorkspaceId(workspace, {
71
+ required: assistantRuntimeConfig.runtimeSurfaceRequiresWorkspace === true
72
+ });
73
+ }
74
+
119
75
  async function createConversationForTurn(workspace, user, options = {}) {
120
- const workspaceId = resolveWorkspaceId(workspace);
76
+ const workspaceId = resolveExpectedWorkspaceId(workspace);
121
77
  const actorUserId = resolveActorUserId(user, {
122
78
  required: true
123
79
  });
@@ -125,7 +81,10 @@ function createTranscriptService({ conversationsRepository, messagesRepository }
125
81
  const conversationId = parsePositiveInteger(source.conversationId);
126
82
 
127
83
  if (conversationId) {
128
- const existing = await conversationsRepository.findByIdForWorkspaceAndUser(conversationId, workspaceId, actorUserId);
84
+ const existing = await conversationsRepository.findByIdForActorScope(conversationId, {
85
+ workspaceId,
86
+ actorUserId
87
+ });
129
88
  if (!existing) {
130
89
  throw new AppError(404, "Conversation not found.");
131
90
  }
@@ -155,7 +114,7 @@ function createTranscriptService({ conversationsRepository, messagesRepository }
155
114
  status: "active",
156
115
  provider: normalizeText(source.provider),
157
116
  model: normalizeText(source.model),
158
- surfaceSid: normalizeText(source.surfaceSid).toLowerCase() || "admin",
117
+ surfaceId: normalizeText(source.surfaceId).toLowerCase() || assistantRuntimeConfig.runtimeSurfaceId,
159
118
  metadata: {
160
119
  firstMessageId: normalizeText(source.messageId)
161
120
  }
@@ -175,12 +134,16 @@ function createTranscriptService({ conversationsRepository, messagesRepository }
175
134
 
176
135
  const source = normalizeObject(payload);
177
136
  const context = normalizeObject(options.context);
178
- const conversation = await conversationsRepository.findById(numericConversationId);
137
+ const actorUserId = parsePositiveInteger(source.actorUserId) || resolveActorUserId(context.actor);
138
+ const workspaceId = resolveExpectedWorkspaceId(options.workspace || context.workspace);
139
+ const conversation = await conversationsRepository.findByIdForActorScope(numericConversationId, {
140
+ workspaceId,
141
+ actorUserId
142
+ });
179
143
  if (!conversation) {
180
144
  throw new AppError(404, "Conversation not found.");
181
145
  }
182
146
 
183
- const actorUserId = parsePositiveInteger(source.actorUserId) || resolveActorUserId(context.actor) || null;
184
147
  const createdMessage = await messagesRepository.create({
185
148
  conversationId: numericConversationId,
186
149
  workspaceId: conversation.workspaceId,
@@ -211,14 +174,22 @@ function createTranscriptService({ conversationsRepository, messagesRepository }
211
174
  };
212
175
  }
213
176
 
214
- async function completeConversation(conversationId, payload = {}) {
177
+ async function completeConversation(conversationId, payload = {}, options = {}) {
215
178
  const numericConversationId = parsePositiveInteger(conversationId);
216
179
  if (!numericConversationId) {
217
180
  throw new TypeError("completeConversation requires conversationId.");
218
181
  }
219
182
 
220
183
  const source = normalizeObject(payload);
221
- const existing = await conversationsRepository.findById(numericConversationId);
184
+ const context = normalizeObject(options.context);
185
+ const actorUserId = resolveActorUserId(context.actor, {
186
+ required: true
187
+ });
188
+ const workspaceId = resolveExpectedWorkspaceId(options.workspace || context.workspace);
189
+ const existing = await conversationsRepository.findByIdForActorScope(numericConversationId, {
190
+ workspaceId,
191
+ actorUserId
192
+ });
222
193
  if (!existing) {
223
194
  throw new AppError(404, "Conversation not found.");
224
195
  }
@@ -236,7 +207,7 @@ function createTranscriptService({ conversationsRepository, messagesRepository }
236
207
  }
237
208
 
238
209
  async function listConversationsForUser(workspace, user, query = {}) {
239
- const workspaceId = resolveWorkspaceId(workspace);
210
+ const workspaceId = resolveExpectedWorkspaceId(workspace);
240
211
  const actorUserId = resolveActorUserId(user, {
241
212
  required: true
242
213
  });
@@ -252,19 +223,21 @@ function createTranscriptService({ conversationsRepository, messagesRepository }
252
223
  ...(status ? { status } : {})
253
224
  };
254
225
 
255
- return conversationsRepository.listForWorkspaceAndUser(
256
- workspaceId,
257
- actorUserId,
226
+ return conversationsRepository.listForActorScope(
258
227
  {
259
- cursor: pagination.cursor,
260
- limit: pagination.limit
261
- },
262
- filters
228
+ workspaceId,
229
+ actorUserId,
230
+ pagination: {
231
+ cursor: pagination.cursor,
232
+ limit: pagination.limit
233
+ },
234
+ filters
235
+ }
263
236
  );
264
237
  }
265
238
 
266
239
  async function getConversationMessagesForUser(workspace, user, conversationId, query = {}) {
267
- const workspaceId = resolveWorkspaceId(workspace);
240
+ const workspaceId = resolveExpectedWorkspaceId(workspace);
268
241
  const actorUserId = resolveActorUserId(user, {
269
242
  required: true
270
243
  });
@@ -279,10 +252,12 @@ function createTranscriptService({ conversationsRepository, messagesRepository }
279
252
  });
280
253
  }
281
254
 
282
- const conversation = await conversationsRepository.findByIdForWorkspaceAndUser(
255
+ const conversation = await conversationsRepository.findByIdForActorScope(
283
256
  numericConversationId,
284
- workspaceId,
285
- actorUserId
257
+ {
258
+ workspaceId,
259
+ actorUserId
260
+ }
286
261
  );
287
262
  if (!conversation) {
288
263
  throw new AppError(404, "Conversation not found.");
@@ -292,12 +267,19 @@ function createTranscriptService({ conversationsRepository, messagesRepository }
292
267
  defaultPageSize: DEFAULT_MESSAGES_PAGE_SIZE,
293
268
  maxPageSize: MAX_MESSAGES_PAGE_SIZE
294
269
  });
295
- const total = await messagesRepository.countByConversationForWorkspace(numericConversationId, workspaceId);
270
+ const total = await messagesRepository.countByConversationScope(
271
+ numericConversationId,
272
+ {
273
+ workspaceId
274
+ }
275
+ );
296
276
  const totalPages = Math.max(1, Math.ceil(total / pagination.pageSize));
297
277
  const page = Math.min(pagination.page, totalPages);
298
- const entries = await messagesRepository.listByConversationForWorkspace(
278
+ const entries = await messagesRepository.listByConversationScope(
299
279
  numericConversationId,
300
- workspaceId,
280
+ {
281
+ workspaceId
282
+ },
301
283
  {
302
284
  page,
303
285
  pageSize: pagination.pageSize
@@ -323,7 +305,4 @@ function createTranscriptService({ conversationsRepository, messagesRepository }
323
305
  });
324
306
  }
325
307
 
326
- export {
327
- createTranscriptService,
328
- serviceEvents
329
- };
308
+ export { createTranscriptService };
@@ -0,0 +1,13 @@
1
+ const assistantRuntimeConfig = Object.freeze({
2
+ runtimeSurfaceId: "__ASSISTANT_RUNTIME_SURFACE_ID__",
3
+ settingsSurfaceId: "__ASSISTANT_SETTINGS_SURFACE_ID__",
4
+ runtimeSurfaceRequiresWorkspace: __ASSISTANT_RUNTIME_SURFACE_REQUIRES_WORKSPACE__,
5
+ settingsSurfaceRequiresWorkspace: __ASSISTANT_SETTINGS_SURFACE_REQUIRES_WORKSPACE__,
6
+ settingsSurfaceRequiresConsoleOwner: __ASSISTANT_SETTINGS_SURFACE_REQUIRES_CONSOLE_OWNER__,
7
+ configScope: "__ASSISTANT_CONFIG_SCOPE__",
8
+ configTable: "__ASSISTANT_CONFIG_TABLE__",
9
+ conversationsTable: "__ASSISTANT_CONVERSATIONS_TABLE__",
10
+ messagesTable: "__ASSISTANT_MESSAGES_TABLE__"
11
+ });
12
+
13
+ export { assistantRuntimeConfig };
@@ -0,0 +1 @@
1
+ export { assistantRuntimeConfig } from "./assistantRuntimeConfig.js";
@@ -0,0 +1,7 @@
1
+ <template>
2
+ <AssistantSurfaceClientElement />
3
+ </template>
4
+
5
+ <script setup>
6
+ import { AssistantSurfaceClientElement } from "@local/assistant/client";
7
+ </script>
@@ -0,0 +1,112 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import test from "node:test";
6
+ import { buildTemplateContext } from "../src/server/buildTemplateContext.js";
7
+
8
+ async function withTempApp(run) {
9
+ const appRoot = await mkdtemp(path.join(tmpdir(), "assistant-generator-"));
10
+ try {
11
+ await mkdir(path.join(appRoot, "config"), { recursive: true });
12
+ await mkdir(path.join(appRoot, "src", "components"), { recursive: true });
13
+ await writeFile(
14
+ path.join(appRoot, "package.json"),
15
+ `${JSON.stringify({ name: "assistant-generator-test-app", private: true, type: "module" }, null, 2)}\n`,
16
+ "utf8"
17
+ );
18
+ await writeFile(
19
+ path.join(appRoot, "config", "public.js"),
20
+ `export const config = {
21
+ surfaceDefinitions: {
22
+ app: { id: "app", enabled: true, requiresWorkspace: false, accessPolicyId: "authenticated" },
23
+ admin: { id: "admin", enabled: true, requiresWorkspace: true, accessPolicyId: "workspace_member" },
24
+ console: { id: "console", enabled: true, requiresWorkspace: false, accessPolicyId: "console_owner" }
25
+ }
26
+ };
27
+ `,
28
+ "utf8"
29
+ );
30
+ await writeFile(
31
+ path.join(appRoot, "src", "components", "ShellLayout.vue"),
32
+ `<template>
33
+ <div>
34
+ <ShellOutlet host="shell-layout" position="primary-menu" />
35
+ <ShellOutlet host="shell-layout" position="top-right" default />
36
+ </div>
37
+ </template>
38
+ `,
39
+ "utf8"
40
+ );
41
+
42
+ return await run(appRoot);
43
+ } finally {
44
+ await rm(appRoot, { recursive: true, force: true });
45
+ }
46
+ }
47
+
48
+ test("buildTemplateContext derives runtime, settings, and table placeholders from explicit surfaces", async () => {
49
+ await withTempApp(async (appRoot) => {
50
+ const context = await buildTemplateContext({
51
+ appRoot,
52
+ options: {
53
+ "runtime-surface": "app",
54
+ "settings-surface": "console",
55
+ "config-scope": "global",
56
+ placement: "shell-layout:primary-menu",
57
+ "menu-label": "Copilot"
58
+ }
59
+ });
60
+
61
+ assert.equal(context.__ASSISTANT_RUNTIME_SURFACE_ID__, "app");
62
+ assert.equal(context.__ASSISTANT_SETTINGS_SURFACE_ID__, "console");
63
+ assert.equal(context.__ASSISTANT_RUNTIME_SURFACE_REQUIRES_WORKSPACE__, "false");
64
+ assert.equal(context.__ASSISTANT_SETTINGS_SURFACE_REQUIRES_WORKSPACE__, "false");
65
+ assert.equal(context.__ASSISTANT_SETTINGS_SURFACE_REQUIRES_CONSOLE_OWNER__, "true");
66
+ assert.equal(context.__ASSISTANT_CONFIG_SCOPE__, "global");
67
+ assert.equal(context.__ASSISTANT_SETTINGS_HOST__, "console-settings");
68
+ assert.equal(context.__ASSISTANT_CONVERSATIONS_TABLE__, "assistant_app_conversations");
69
+ assert.equal(context.__ASSISTANT_MESSAGES_TABLE__, "assistant_app_messages");
70
+ assert.equal(context.__ASSISTANT_MENU_PLACEMENT_HOST__, "shell-layout");
71
+ assert.equal(context.__ASSISTANT_MENU_PLACEMENT_POSITION__, "primary-menu");
72
+ assert.equal(context.__ASSISTANT_MENU_LABEL__, "Copilot");
73
+ });
74
+ });
75
+
76
+ test("buildTemplateContext rejects workspace config scope for non-workspace surfaces", async () => {
77
+ await withTempApp(async (appRoot) => {
78
+ await assert.rejects(
79
+ () =>
80
+ buildTemplateContext({
81
+ appRoot,
82
+ options: {
83
+ "runtime-surface": "app",
84
+ "settings-surface": "console",
85
+ "config-scope": "workspace",
86
+ placement: "shell-layout:primary-menu"
87
+ }
88
+ }),
89
+ /config-scope "workspace" requires runtime surface "app" with requiresWorkspace=true/
90
+ );
91
+ });
92
+ });
93
+
94
+ test("buildTemplateContext accepts workspace config scope when both surfaces require workspace", async () => {
95
+ await withTempApp(async (appRoot) => {
96
+ const context = await buildTemplateContext({
97
+ appRoot,
98
+ options: {
99
+ "runtime-surface": "admin",
100
+ "settings-surface": "admin",
101
+ "config-scope": "workspace",
102
+ placement: "shell-layout:top-right"
103
+ }
104
+ });
105
+
106
+ assert.equal(context.__ASSISTANT_RUNTIME_SURFACE_REQUIRES_WORKSPACE__, "true");
107
+ assert.equal(context.__ASSISTANT_SETTINGS_SURFACE_REQUIRES_WORKSPACE__, "true");
108
+ assert.equal(context.__ASSISTANT_SETTINGS_HOST__, "admin-settings");
109
+ assert.equal(context.__ASSISTANT_CONVERSATIONS_TABLE__, "assistant_admin_conversations");
110
+ assert.equal(context.__ASSISTANT_MESSAGES_TABLE__, "assistant_admin_messages");
111
+ });
112
+ });
@@ -0,0 +1,69 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import descriptor from "../package.descriptor.mjs";
4
+
5
+ function findFileMutation(id) {
6
+ const mutations = Array.isArray(descriptor?.mutations?.files) ? descriptor.mutations.files : [];
7
+ return mutations.find((entry) => String(entry?.id || "") === id) || null;
8
+ }
9
+
10
+ function findTextMutation(id) {
11
+ const mutations = Array.isArray(descriptor?.mutations?.text) ? descriptor.mutations.text : [];
12
+ return mutations.find((entry) => String(entry?.id || "") === id) || null;
13
+ }
14
+
15
+ test("assistant descriptor exposes generator options for runtime surface, settings surface, and config scope", () => {
16
+ assert.equal(descriptor.kind, "generator");
17
+ assert.equal(descriptor.metadata?.generatorPrimarySubcommand, "install");
18
+ assert.equal(descriptor.options?.["runtime-surface"]?.required, true);
19
+ assert.equal(descriptor.options?.["settings-surface"]?.required, true);
20
+ assert.equal(descriptor.options?.["config-scope"]?.defaultValue, "global");
21
+ });
22
+
23
+ test("assistant descriptor publishes install examples for the supported runtime/settings shapes", () => {
24
+ const installMetadata = descriptor.metadata?.generatorSubcommands?.install || {};
25
+ const examples = Array.isArray(installMetadata.examples) ? installMetadata.examples : [];
26
+ const labels = examples.map((entry) => String(entry?.label || ""));
27
+
28
+ assert.equal(examples.length, 4);
29
+ assert.deepEqual(labels, [
30
+ "App runtime, console settings, global config",
31
+ "App runtime, app settings, global config",
32
+ "Workspace runtime, console settings, global config",
33
+ "Workspace runtime, workspace settings, workspace config"
34
+ ]);
35
+ });
36
+
37
+ test("assistant descriptor installs generated local runtime files instead of shipping a framework runtime", () => {
38
+ const runtimePage = findFileMutation("assistant-page-runtime");
39
+ const localPackageDescriptor = findFileMutation("assistant-local-package-descriptor");
40
+ const configMigration = findFileMutation("assistant-config-initial-schema");
41
+ const transcriptMigration = findFileMutation("assistant-transcripts-initial-schema");
42
+
43
+ assert.equal(runtimePage?.toSurface, "${option:runtime-surface|lower}");
44
+ assert.equal(runtimePage?.toSurfacePath, "assistant/index.vue");
45
+ assert.equal(localPackageDescriptor?.to, "packages/assistant/package.descriptor.mjs");
46
+ assert.equal(configMigration?.from, "templates/migrations/assistant_config_initial.cjs");
47
+ assert.equal(transcriptMigration?.from, "templates/migrations/assistant_transcripts_initial.cjs");
48
+ });
49
+
50
+ test("assistant descriptor targets runtime and settings placements through explicit surface options", () => {
51
+ const menuPlacement = findTextMutation("assistant-placement-menu");
52
+ const settingsPlacement = findTextMutation("assistant-settings-form-placement");
53
+
54
+ assert.match(String(menuPlacement?.value || ""), /surfaces: \["\$\{option:runtime-surface\|lower\}"\]/);
55
+ assert.match(String(menuPlacement?.value || ""), /workspaceSuffix: "__ASSISTANT_MENU_WORKSPACE_SUFFIX__"/);
56
+ assert.match(String(menuPlacement?.value || ""), /nonWorkspaceSuffix: "__ASSISTANT_MENU_NON_WORKSPACE_SUFFIX__"/);
57
+ assert.match(String(settingsPlacement?.value || ""), /host: "__ASSISTANT_SETTINGS_HOST__"/);
58
+ assert.match(String(settingsPlacement?.value || ""), /surfaces: \["\$\{option:settings-surface\|lower\}"\]/);
59
+ assert.match(String(settingsPlacement?.value || ""), /componentToken: "assistant.web.settings.element"/);
60
+ });
61
+
62
+ test("assistant descriptor runtime deps use assistant-core and avoid workspace package coupling", () => {
63
+ const runtimeDeps = descriptor?.mutations?.dependencies?.runtime || {};
64
+
65
+ assert.equal(runtimeDeps["@local/assistant"], "file:packages/assistant");
66
+ assert.equal(runtimeDeps["@jskit-ai/assistant-core"], "0.1.0");
67
+ assert.equal(Object.hasOwn(runtimeDeps, "@jskit-ai/workspaces-core"), false);
68
+ assert.equal(Object.hasOwn(runtimeDeps, "@jskit-ai/workspaces-web"), false);
69
+ });