@jskit-ai/assistant 0.1.4

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 (57) hide show
  1. package/package.descriptor.mjs +284 -0
  2. package/package.json +31 -0
  3. package/src/client/components/AssistantClientElement.vue +1316 -0
  4. package/src/client/components/AssistantConsoleSettingsClientElement.vue +71 -0
  5. package/src/client/components/AssistantSettingsFormCard.vue +76 -0
  6. package/src/client/components/AssistantWorkspaceClientElement.vue +15 -0
  7. package/src/client/components/AssistantWorkspaceSettingsClientElement.vue +73 -0
  8. package/src/client/composables/useAssistantWorkspaceRuntime.js +789 -0
  9. package/src/client/index.js +12 -0
  10. package/src/client/lib/assistantApi.js +137 -0
  11. package/src/client/lib/assistantHttpClient.js +10 -0
  12. package/src/client/lib/markdownRenderer.js +31 -0
  13. package/src/client/providers/AssistantWebClientProvider.js +25 -0
  14. package/src/server/AssistantServiceProvider.js +179 -0
  15. package/src/server/actionIds.js +11 -0
  16. package/src/server/actions.js +191 -0
  17. package/src/server/diTokens.js +19 -0
  18. package/src/server/lib/aiClient.js +43 -0
  19. package/src/server/lib/ndjson.js +47 -0
  20. package/src/server/lib/providers/anthropicClient.js +375 -0
  21. package/src/server/lib/providers/common.js +158 -0
  22. package/src/server/lib/providers/deepSeekClient.js +22 -0
  23. package/src/server/lib/providers/openAiClient.js +13 -0
  24. package/src/server/lib/providers/openAiCompatibleClient.js +69 -0
  25. package/src/server/lib/resolveWorkspaceSlug.js +24 -0
  26. package/src/server/lib/serviceToolCatalog.js +459 -0
  27. package/src/server/registerRoutes.js +384 -0
  28. package/src/server/repositories/assistantSettingsRepository.js +100 -0
  29. package/src/server/repositories/conversationsRepository.js +244 -0
  30. package/src/server/repositories/messagesRepository.js +154 -0
  31. package/src/server/repositories/repositoryPersistenceUtils.js +63 -0
  32. package/src/server/services/assistantSettingsService.js +153 -0
  33. package/src/server/services/chatService.js +987 -0
  34. package/src/server/services/transcriptService.js +334 -0
  35. package/src/shared/assistantPaths.js +50 -0
  36. package/src/shared/assistantResource.js +323 -0
  37. package/src/shared/assistantSettingsResource.js +214 -0
  38. package/src/shared/index.js +39 -0
  39. package/src/shared/queryKeys.js +69 -0
  40. package/src/shared/settingsEvents.js +7 -0
  41. package/src/shared/streamEvents.js +31 -0
  42. package/src/shared/support/positiveInteger.js +9 -0
  43. package/templates/migrations/assistant_settings_initial.cjs +39 -0
  44. package/templates/migrations/assistant_transcripts_initial.cjs +51 -0
  45. package/templates/src/pages/admin/workspace/assistant/index.vue +7 -0
  46. package/test/aiConfigValidation.test.js +15 -0
  47. package/test/assistantApiSurfaceHeader.test.js +64 -0
  48. package/test/assistantResource.test.js +53 -0
  49. package/test/assistantSettingsResource.test.js +48 -0
  50. package/test/assistantSettingsService.test.js +133 -0
  51. package/test/chatService.test.js +841 -0
  52. package/test/descriptorSurfaceOption.test.js +35 -0
  53. package/test/queryKeys.test.js +41 -0
  54. package/test/resolveWorkspaceSlug.test.js +83 -0
  55. package/test/routeInputContracts.test.js +287 -0
  56. package/test/serviceToolCatalog.test.js +1235 -0
  57. package/test/transcriptService.test.js +175 -0
@@ -0,0 +1,214 @@
1
+ import { Type } from "typebox";
2
+ import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators";
3
+ import { normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
4
+
5
+ const MAX_SYSTEM_PROMPT_CHARS = 12_000;
6
+
7
+ function createPromptSchema(promptLabel) {
8
+ return Type.String({
9
+ maxLength: MAX_SYSTEM_PROMPT_CHARS,
10
+ messages: {
11
+ maxLength: `${promptLabel} must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters.`,
12
+ default: `${promptLabel} must be valid text.`
13
+ }
14
+ });
15
+ }
16
+
17
+ function createPromptSettingsResource({
18
+ resourceId = "",
19
+ fields = [],
20
+ validationMessage = "Fix invalid values and try again.",
21
+ saveSuccessMessage = "Saved.",
22
+ saveErrorMessage = "Unable to save settings."
23
+ } = {}) {
24
+ const settingsOutputProperties = {};
25
+ const settingsCreateProperties = {};
26
+ for (const field of fields) {
27
+ settingsOutputProperties[field.key] = field.outputSchema;
28
+ settingsCreateProperties[field.key] =
29
+ field.required === false ? Type.Optional(field.inputSchema) : field.inputSchema;
30
+ }
31
+
32
+ const settingsOutputSchema = Type.Object(settingsOutputProperties, { additionalProperties: false });
33
+ const settingsCreateSchema = Type.Object(settingsCreateProperties, { additionalProperties: false });
34
+ const settingsPatchSchema = Type.Partial(settingsCreateSchema, {
35
+ additionalProperties: false
36
+ });
37
+ const recordSchema = Type.Object(
38
+ {
39
+ settings: settingsOutputSchema
40
+ },
41
+ { additionalProperties: false }
42
+ );
43
+
44
+ function normalizeInput(payload = {}) {
45
+ const source = normalizeObjectInput(payload);
46
+ const normalized = {};
47
+ for (const field of fields) {
48
+ if (!Object.hasOwn(source, field.key)) {
49
+ continue;
50
+ }
51
+ normalized[field.key] = field.normalizeInput(source[field.key], {
52
+ payload: source
53
+ });
54
+ }
55
+ return normalized;
56
+ }
57
+
58
+ function normalizeOutput(payload = {}) {
59
+ const source = normalizeObjectInput(payload);
60
+ const settingsSource = normalizeObjectInput(source.settings);
61
+ const settings = {};
62
+ for (const field of fields) {
63
+ const rawValue = Object.hasOwn(settingsSource, field.key)
64
+ ? settingsSource[field.key]
65
+ : field.resolveDefault({
66
+ settings: settingsSource
67
+ });
68
+ settings[field.key] = field.normalizeOutput(rawValue, {
69
+ settings: settingsSource
70
+ });
71
+ }
72
+ return {
73
+ settings
74
+ };
75
+ }
76
+
77
+ const outputValidator = Object.freeze({
78
+ schema: recordSchema,
79
+ normalize: normalizeOutput
80
+ });
81
+
82
+ return Object.freeze({
83
+ resource: resourceId,
84
+ messages: {
85
+ validation: validationMessage,
86
+ saveSuccess: saveSuccessMessage,
87
+ saveError: saveErrorMessage
88
+ },
89
+ operations: Object.freeze({
90
+ view: Object.freeze({
91
+ method: "GET",
92
+ outputValidator
93
+ }),
94
+ create: Object.freeze({
95
+ method: "POST",
96
+ bodyValidator: Object.freeze({
97
+ schema: settingsCreateSchema,
98
+ normalize: normalizeInput
99
+ }),
100
+ outputValidator
101
+ }),
102
+ replace: Object.freeze({
103
+ method: "PUT",
104
+ bodyValidator: Object.freeze({
105
+ schema: settingsCreateSchema,
106
+ normalize: normalizeInput
107
+ }),
108
+ outputValidator
109
+ }),
110
+ patch: Object.freeze({
111
+ method: "PATCH",
112
+ bodyValidator: Object.freeze({
113
+ schema: settingsPatchSchema,
114
+ normalize: normalizeInput
115
+ }),
116
+ outputValidator
117
+ })
118
+ })
119
+ });
120
+ }
121
+
122
+ function createFieldRegistry(scopeLabel) {
123
+ const fields = [];
124
+
125
+ function defineField(field = {}) {
126
+ const key = normalizeText(field.key);
127
+ if (!key) {
128
+ throw new TypeError(`${scopeLabel}.defineField requires field.key.`);
129
+ }
130
+ if (fields.some((entry) => entry.key === key)) {
131
+ throw new Error(`${scopeLabel}.defineField duplicate key: ${key}`);
132
+ }
133
+ if (!field.inputSchema || typeof field.inputSchema !== "object") {
134
+ throw new TypeError(`${scopeLabel}.defineField("${key}") requires inputSchema.`);
135
+ }
136
+ if (!field.outputSchema || typeof field.outputSchema !== "object") {
137
+ throw new TypeError(`${scopeLabel}.defineField("${key}") requires outputSchema.`);
138
+ }
139
+ if (typeof field.normalizeInput !== "function") {
140
+ throw new TypeError(`${scopeLabel}.defineField("${key}") requires normalizeInput.`);
141
+ }
142
+ if (typeof field.normalizeOutput !== "function") {
143
+ throw new TypeError(`${scopeLabel}.defineField("${key}") requires normalizeOutput.`);
144
+ }
145
+ if (typeof field.resolveDefault !== "function") {
146
+ throw new TypeError(`${scopeLabel}.defineField("${key}") requires resolveDefault.`);
147
+ }
148
+
149
+ fields.push({
150
+ key,
151
+ required: field.required !== false,
152
+ inputSchema: field.inputSchema,
153
+ outputSchema: field.outputSchema,
154
+ normalizeInput: field.normalizeInput,
155
+ normalizeOutput: field.normalizeOutput,
156
+ resolveDefault: field.resolveDefault
157
+ });
158
+ }
159
+
160
+ return {
161
+ fields,
162
+ defineField
163
+ };
164
+ }
165
+
166
+ const assistantConsoleSettingsFields = (() => {
167
+ const registry = createFieldRegistry("assistantConsoleSettingsFields");
168
+ const { fields, defineField } = registry;
169
+ defineField({
170
+ key: "workspaceSurfacePrompt",
171
+ required: true,
172
+ inputSchema: createPromptSchema("Workspace surface system prompt"),
173
+ outputSchema: Type.String({ maxLength: MAX_SYSTEM_PROMPT_CHARS }),
174
+ normalizeInput: (value) => String(value || ""),
175
+ normalizeOutput: (value) => String(value || ""),
176
+ resolveDefault: () => ""
177
+ });
178
+ return fields;
179
+ })();
180
+
181
+ const assistantWorkspaceSettingsFields = (() => {
182
+ const registry = createFieldRegistry("assistantWorkspaceSettingsFields");
183
+ const { fields, defineField } = registry;
184
+ defineField({
185
+ key: "appSurfacePrompt",
186
+ required: true,
187
+ inputSchema: createPromptSchema("App surface system prompt"),
188
+ outputSchema: Type.String({ maxLength: MAX_SYSTEM_PROMPT_CHARS }),
189
+ normalizeInput: (value) => String(value || ""),
190
+ normalizeOutput: (value) => String(value || ""),
191
+ resolveDefault: () => ""
192
+ });
193
+ return fields;
194
+ })();
195
+
196
+ const assistantConsoleSettingsResource = createPromptSettingsResource({
197
+ resourceId: "assistantConsoleSettings",
198
+ fields: assistantConsoleSettingsFields,
199
+ saveSuccessMessage: "Assistant console settings updated.",
200
+ saveErrorMessage: "Unable to update assistant console settings."
201
+ });
202
+
203
+ const assistantWorkspaceSettingsResource = createPromptSettingsResource({
204
+ resourceId: "assistantWorkspaceSettings",
205
+ fields: assistantWorkspaceSettingsFields,
206
+ saveSuccessMessage: "Assistant workspace settings updated.",
207
+ saveErrorMessage: "Unable to update assistant workspace settings."
208
+ });
209
+
210
+ export {
211
+ MAX_SYSTEM_PROMPT_CHARS,
212
+ assistantConsoleSettingsResource,
213
+ assistantWorkspaceSettingsResource
214
+ };
@@ -0,0 +1,39 @@
1
+ export {
2
+ ASSISTANT_API_RELATIVE_PATH,
3
+ resolveAssistantApiBasePath,
4
+ resolveAssistantWorkspaceApiBasePath,
5
+ buildAssistantWorkspaceApiPath
6
+ } from "./assistantPaths.js";
7
+
8
+ export {
9
+ ASSISTANT_QUERY_KEY_PREFIX,
10
+ assistantRootQueryKey,
11
+ assistantWorkspaceScopeQueryKey,
12
+ assistantConversationsListQueryKey,
13
+ assistantConversationMessagesQueryKey
14
+ } from "./queryKeys.js";
15
+
16
+ export {
17
+ ASSISTANT_STREAM_EVENT_TYPES,
18
+ ASSISTANT_TRANSCRIPT_CHANGED_EVENT,
19
+ normalizeAssistantStreamEventType
20
+ } from "./streamEvents.js";
21
+
22
+ export {
23
+ MAX_INPUT_CHARS,
24
+ MAX_HISTORY_MESSAGES,
25
+ assistantResource
26
+ } from "./assistantResource.js";
27
+
28
+ export {
29
+ MAX_SYSTEM_PROMPT_CHARS,
30
+ assistantConsoleSettingsResource,
31
+ assistantWorkspaceSettingsResource
32
+ } from "./assistantSettingsResource.js";
33
+
34
+ export {
35
+ ASSISTANT_CONSOLE_SETTINGS_CHANGED_EVENT,
36
+ ASSISTANT_WORKSPACE_SETTINGS_CHANGED_EVENT
37
+ } from "./settingsEvents.js";
38
+
39
+ export { toPositiveInteger } from "./support/positiveInteger.js";
@@ -0,0 +1,69 @@
1
+ const ASSISTANT_QUERY_KEY_PREFIX = Object.freeze(["assistant"]);
2
+
3
+ function normalizeWorkspaceSlug(value) {
4
+ return String(value || "").trim() || "none";
5
+ }
6
+
7
+ function normalizePositiveInteger(value, fallback) {
8
+ const parsed = Number(value);
9
+ if (!Number.isInteger(parsed) || parsed < 1) {
10
+ return fallback;
11
+ }
12
+
13
+ return parsed;
14
+ }
15
+
16
+ function normalizeWorkspaceScope({ workspaceSlug = "", workspaceId = 0 } = {}) {
17
+ const normalizedWorkspaceId = normalizePositiveInteger(workspaceId, 0);
18
+ if (normalizedWorkspaceId > 0) {
19
+ return `id:${normalizedWorkspaceId}`;
20
+ }
21
+
22
+ return `slug:${normalizeWorkspaceSlug(workspaceSlug)}`;
23
+ }
24
+
25
+ function normalizeStatus(value) {
26
+ const normalized = String(value || "").trim().toLowerCase();
27
+ return normalized || "all";
28
+ }
29
+
30
+ function normalizeConversationId(value) {
31
+ return String(normalizePositiveInteger(value, 0) || "none");
32
+ }
33
+
34
+ function assistantRootQueryKey() {
35
+ return [...ASSISTANT_QUERY_KEY_PREFIX];
36
+ }
37
+
38
+ function assistantWorkspaceScopeQueryKey(workspaceScope = {}) {
39
+ return [...assistantRootQueryKey(), normalizeWorkspaceScope(workspaceScope)];
40
+ }
41
+
42
+ function assistantConversationsListQueryKey(workspaceScope = {}, { limit = 20, status = "" } = {}) {
43
+ return [
44
+ ...assistantWorkspaceScopeQueryKey(workspaceScope),
45
+ "conversations",
46
+ "list",
47
+ normalizePositiveInteger(limit, 20),
48
+ normalizeStatus(status)
49
+ ];
50
+ }
51
+
52
+ function assistantConversationMessagesQueryKey(workspaceScope = {}, conversationId, { page = 1, pageSize = 200 } = {}) {
53
+ return [
54
+ ...assistantWorkspaceScopeQueryKey(workspaceScope),
55
+ "conversations",
56
+ normalizeConversationId(conversationId),
57
+ "messages",
58
+ normalizePositiveInteger(page, 1),
59
+ normalizePositiveInteger(pageSize, 200)
60
+ ];
61
+ }
62
+
63
+ export {
64
+ ASSISTANT_QUERY_KEY_PREFIX,
65
+ assistantRootQueryKey,
66
+ assistantWorkspaceScopeQueryKey,
67
+ assistantConversationsListQueryKey,
68
+ assistantConversationMessagesQueryKey
69
+ };
@@ -0,0 +1,7 @@
1
+ const ASSISTANT_CONSOLE_SETTINGS_CHANGED_EVENT = "assistant.console.settings.changed";
2
+ const ASSISTANT_WORKSPACE_SETTINGS_CHANGED_EVENT = "assistant.workspace.settings.changed";
3
+
4
+ export {
5
+ ASSISTANT_CONSOLE_SETTINGS_CHANGED_EVENT,
6
+ ASSISTANT_WORKSPACE_SETTINGS_CHANGED_EVENT
7
+ };
@@ -0,0 +1,31 @@
1
+ const ASSISTANT_STREAM_EVENT_TYPES = Object.freeze({
2
+ META: "meta",
3
+ ASSISTANT_DELTA: "assistant_delta",
4
+ ASSISTANT_MESSAGE: "assistant_message",
5
+ TOOL_CALL: "tool_call",
6
+ TOOL_RESULT: "tool_result",
7
+ ERROR: "error",
8
+ DONE: "done"
9
+ });
10
+
11
+ const ASSISTANT_TRANSCRIPT_CHANGED_EVENT = "assistant.transcript.changed";
12
+ const STREAM_EVENT_TYPE_SET = new Set(Object.values(ASSISTANT_STREAM_EVENT_TYPES));
13
+
14
+ function normalizeAssistantStreamEventType(value, fallback = "") {
15
+ const normalized = String(value || "").trim().toLowerCase();
16
+ if (!normalized) {
17
+ return fallback;
18
+ }
19
+
20
+ if (!STREAM_EVENT_TYPE_SET.has(normalized)) {
21
+ return fallback;
22
+ }
23
+
24
+ return normalized;
25
+ }
26
+
27
+ export {
28
+ ASSISTANT_STREAM_EVENT_TYPES,
29
+ ASSISTANT_TRANSCRIPT_CHANGED_EVENT,
30
+ normalizeAssistantStreamEventType
31
+ };
@@ -0,0 +1,9 @@
1
+ import { normalizePositiveInteger } from "@jskit-ai/kernel/shared/support/normalize";
2
+
3
+ function toPositiveInteger(value, fallback = 0) {
4
+ return normalizePositiveInteger(value, {
5
+ fallback
6
+ });
7
+ }
8
+
9
+ export { toPositiveInteger };
@@ -0,0 +1,39 @@
1
+ // JSKIT_MIGRATION_ID: assistant-settings-initial-schema
2
+
3
+ /**
4
+ * @param {import('knex').Knex} knex
5
+ */
6
+ exports.up = async function up(knex) {
7
+ const hasConsolePromptColumn = await knex.schema.hasColumn("console_settings", "assistant_workspace_surface_prompt");
8
+ if (!hasConsolePromptColumn) {
9
+ await knex.schema.alterTable("console_settings", (table) => {
10
+ table.text("assistant_workspace_surface_prompt").notNullable().defaultTo("");
11
+ });
12
+ }
13
+
14
+ const hasWorkspacePromptColumn = await knex.schema.hasColumn("workspace_settings", "assistant_app_surface_prompt");
15
+ if (!hasWorkspacePromptColumn) {
16
+ await knex.schema.alterTable("workspace_settings", (table) => {
17
+ table.text("assistant_app_surface_prompt").notNullable().defaultTo("");
18
+ });
19
+ }
20
+ };
21
+
22
+ /**
23
+ * @param {import('knex').Knex} knex
24
+ */
25
+ exports.down = async function down(knex) {
26
+ const hasWorkspacePromptColumn = await knex.schema.hasColumn("workspace_settings", "assistant_app_surface_prompt");
27
+ if (hasWorkspacePromptColumn) {
28
+ await knex.schema.alterTable("workspace_settings", (table) => {
29
+ table.dropColumn("assistant_app_surface_prompt");
30
+ });
31
+ }
32
+
33
+ const hasConsolePromptColumn = await knex.schema.hasColumn("console_settings", "assistant_workspace_surface_prompt");
34
+ if (hasConsolePromptColumn) {
35
+ await knex.schema.alterTable("console_settings", (table) => {
36
+ table.dropColumn("assistant_workspace_surface_prompt");
37
+ });
38
+ }
39
+ };
@@ -0,0 +1,51 @@
1
+ // JSKIT_MIGRATION_ID: assistant_transcripts_initial
2
+
3
+ exports.up = async function up(knex) {
4
+ const hasConversationsTable = await knex.schema.hasTable("ai_conversations");
5
+ if (!hasConversationsTable) {
6
+ await knex.schema.createTable("ai_conversations", (table) => {
7
+ table.increments("id").unsigned().primary();
8
+ table.integer("workspace_id").unsigned().notNullable().index();
9
+ table.integer("created_by_user_id").unsigned().nullable().index();
10
+ table.string("title", 160).notNullable().defaultTo("New conversation");
11
+ table.string("status", 32).notNullable().defaultTo("active");
12
+ table.string("provider", 64).notNullable().defaultTo("");
13
+ table.string("model", 128).notNullable().defaultTo("");
14
+ table.string("surface_id", 32).notNullable().defaultTo("admin");
15
+ table.integer("message_count").unsigned().notNullable().defaultTo(0);
16
+ table.text("metadata_json").nullable();
17
+ table.timestamp("started_at").notNullable().defaultTo(knex.fn.now());
18
+ table.timestamp("ended_at").nullable();
19
+ table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
20
+ table.timestamp("updated_at").notNullable().defaultTo(knex.fn.now());
21
+
22
+ table.index(["workspace_id", "started_at"], "idx_ai_conversations_workspace_started_at");
23
+ table.index(["workspace_id", "created_by_user_id"], "idx_ai_conversations_workspace_creator");
24
+ });
25
+ }
26
+
27
+ const hasMessagesTable = await knex.schema.hasTable("ai_messages");
28
+ if (!hasMessagesTable) {
29
+ await knex.schema.createTable("ai_messages", (table) => {
30
+ table.increments("id").unsigned().primary();
31
+ table.integer("conversation_id").unsigned().notNullable().references("id").inTable("ai_conversations").onDelete("CASCADE");
32
+ table.integer("workspace_id").unsigned().notNullable().index();
33
+ table.integer("seq").unsigned().notNullable();
34
+ table.string("role", 32).notNullable();
35
+ table.string("kind", 32).notNullable().defaultTo("chat");
36
+ table.string("client_message_id", 128).notNullable().defaultTo("");
37
+ table.integer("actor_user_id").unsigned().nullable().index();
38
+ table.text("content_text").nullable();
39
+ table.text("metadata_json").nullable();
40
+ table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
41
+
42
+ table.unique(["conversation_id", "seq"], "uq_ai_messages_conversation_seq");
43
+ table.index(["conversation_id", "created_at"], "idx_ai_messages_conversation_created_at");
44
+ });
45
+ }
46
+ };
47
+
48
+ exports.down = async function down(knex) {
49
+ await knex.schema.dropTableIfExists("ai_messages");
50
+ await knex.schema.dropTableIfExists("ai_conversations");
51
+ };
@@ -0,0 +1,7 @@
1
+ <template>
2
+ <AssistantWorkspaceClientElement />
3
+ </template>
4
+
5
+ <script setup>
6
+ import { AssistantWorkspaceClientElement } from "@jskit-ai/assistant/client";
7
+ </script>
@@ -0,0 +1,15 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { normalizeOptionalHttpUrl } from "../src/server/lib/providers/common.js";
4
+
5
+ test("normalizeOptionalHttpUrl accepts empty values", () => {
6
+ assert.equal(normalizeOptionalHttpUrl(""), "");
7
+ assert.equal(normalizeOptionalHttpUrl(" "), "");
8
+ });
9
+
10
+ test("normalizeOptionalHttpUrl rejects non-http absolute values", () => {
11
+ assert.throws(
12
+ () => normalizeOptionalHttpUrl("cd ../89", { context: "assistant AI_BASE_URL" }),
13
+ /assistant AI_BASE_URL must be an absolute http\(s\) URL\./
14
+ );
15
+ });
@@ -0,0 +1,64 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { createAssistantWorkspaceApi } from "../src/client/lib/assistantApi.js";
4
+
5
+ test("assistant workspace api forwards normalized surface header on requests", async () => {
6
+ const observed = {
7
+ stream: null,
8
+ list: null,
9
+ messages: null
10
+ };
11
+
12
+ const api = createAssistantWorkspaceApi({
13
+ resolveSurfaceId: () => "AdMiN",
14
+ async request(url, options = {}) {
15
+ if (url.includes("/messages")) {
16
+ observed.messages = { url, options };
17
+ } else {
18
+ observed.list = { url, options };
19
+ }
20
+ return {};
21
+ },
22
+ async requestStream(url, options = {}) {
23
+ observed.stream = { url, options };
24
+ return null;
25
+ }
26
+ });
27
+
28
+ await api.streamChat("acme", {
29
+ messageId: "msg_1",
30
+ input: "Hello"
31
+ });
32
+ await api.listConversations("acme", {
33
+ limit: 5
34
+ });
35
+ await api.getConversationMessages("acme", 99, {
36
+ page: 1,
37
+ pageSize: 5
38
+ });
39
+
40
+ assert.equal(observed.stream?.options?.headers?.["x-jskit-surface"], "admin");
41
+ assert.equal(observed.list?.options?.headers?.["x-jskit-surface"], "admin");
42
+ assert.equal(observed.messages?.options?.headers?.["x-jskit-surface"], "admin");
43
+ });
44
+
45
+ test("assistant workspace api omits surface header when surface id is empty", async () => {
46
+ const observed = [];
47
+ const api = createAssistantWorkspaceApi({
48
+ resolveSurfaceId: () => "",
49
+ async request(url, options = {}) {
50
+ observed.push({
51
+ url,
52
+ options
53
+ });
54
+ return {};
55
+ },
56
+ async requestStream(_url, _options = {}) {
57
+ return null;
58
+ }
59
+ });
60
+
61
+ await api.listConversations("acme");
62
+ assert.equal(observed.length, 1);
63
+ assert.equal(Object.hasOwn(observed[0].options || {}, "headers"), false);
64
+ });
@@ -0,0 +1,53 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { Check } from "typebox/value";
4
+ import { assistantResource } from "../src/shared/assistantResource.js";
5
+
6
+ test("assistant output schemas accept normalized paginated payloads", () => {
7
+ const conversationsListSchema = assistantResource.operations.conversationsList.outputValidator.schema;
8
+ const conversationMessagesSchema = assistantResource.operations.conversationMessagesList.outputValidator.schema;
9
+
10
+ const conversationsPayload = {
11
+ items: [],
12
+ nextCursor: null
13
+ };
14
+
15
+ const messagesPayload = {
16
+ conversation: {
17
+ id: 1,
18
+ workspaceId: 10,
19
+ workspaceSlug: "acme",
20
+ workspaceName: "Acme",
21
+ title: "Conversation",
22
+ createdByUserId: 7,
23
+ createdByUserDisplayName: "Merc",
24
+ createdByUserEmail: "merc@example.com",
25
+ status: "active",
26
+ provider: "openai",
27
+ model: "gpt-4.1",
28
+ surfaceId: "admin",
29
+ startedAt: "2026-03-16T10:00:00.000Z",
30
+ endedAt: null,
31
+ messageCount: 2,
32
+ metadata: {},
33
+ createdAt: "2026-03-16T10:00:00.000Z",
34
+ updatedAt: "2026-03-16T10:01:00.000Z"
35
+ },
36
+ entries: [],
37
+ page: 1,
38
+ pageSize: 200,
39
+ total: 0,
40
+ totalPages: 1
41
+ };
42
+
43
+ assert.equal(Check(conversationsListSchema, conversationsPayload), true);
44
+ assert.equal(Check(conversationMessagesSchema, messagesPayload), true);
45
+ });
46
+
47
+ test("assistant conversation message params accept numeric path strings and normalize to integer", () => {
48
+ const paramsValidator = assistantResource.operations.conversationMessagesList.paramsValidator;
49
+ assert.equal(Check(paramsValidator.schema, { conversationId: "2" }), true);
50
+ assert.deepEqual(paramsValidator.normalize({ conversationId: "2" }), {
51
+ conversationId: 2
52
+ });
53
+ });
@@ -0,0 +1,48 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { Check } from "typebox/value";
4
+ import { validateOperationSection } from "@jskit-ai/http-runtime/shared/validators/operationValidation";
5
+ import {
6
+ assistantConsoleSettingsResource,
7
+ assistantWorkspaceSettingsResource
8
+ } from "../src/shared/assistantSettingsResource.js";
9
+
10
+ test("assistant settings resources expose valid output schemas", () => {
11
+ const consoleSchema = assistantConsoleSettingsResource.operations.view.outputValidator.schema;
12
+ const workspaceSchema = assistantWorkspaceSettingsResource.operations.view.outputValidator.schema;
13
+
14
+ assert.equal(
15
+ Check(consoleSchema, {
16
+ settings: {
17
+ workspaceSurfacePrompt: ""
18
+ }
19
+ }),
20
+ true
21
+ );
22
+ assert.equal(
23
+ Check(workspaceSchema, {
24
+ settings: {
25
+ appSurfacePrompt: ""
26
+ }
27
+ }),
28
+ true
29
+ );
30
+ });
31
+
32
+ test("assistant settings patch normalizer preserves omitted fields", () => {
33
+ const consolePatch = validateOperationSection({
34
+ operation: assistantConsoleSettingsResource.operations.patch,
35
+ section: "bodyValidator",
36
+ value: {}
37
+ });
38
+ const workspacePatch = validateOperationSection({
39
+ operation: assistantWorkspaceSettingsResource.operations.patch,
40
+ section: "bodyValidator",
41
+ value: {}
42
+ });
43
+
44
+ assert.equal(consolePatch.ok, true);
45
+ assert.equal(workspacePatch.ok, true);
46
+ assert.deepEqual(consolePatch.value, {});
47
+ assert.deepEqual(workspacePatch.value, {});
48
+ });