@ysk-kit/cli 1.2.1 → 1.2.2

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 (68) hide show
  1. package/dist/add-capability.js +2 -2
  2. package/dist/add-capability.js.map +1 -1
  3. package/dist/add-module.js +7 -7
  4. package/dist/add-module.js.map +1 -1
  5. package/dist/apply-capability.d.ts +1 -1
  6. package/dist/apply-capability.d.ts.map +1 -1
  7. package/dist/apply-capability.js +3 -3
  8. package/dist/apply-capability.js.map +1 -1
  9. package/dist/capability-catalog.d.ts +1 -1
  10. package/dist/capability-catalog.d.ts.map +1 -1
  11. package/dist/capability-catalog.js +11 -3
  12. package/dist/capability-catalog.js.map +1 -1
  13. package/dist/capability-patches.d.ts.map +1 -1
  14. package/dist/capability-patches.js +29 -46
  15. package/dist/capability-patches.js.map +1 -1
  16. package/dist/check-agent-guidance.d.ts.map +1 -1
  17. package/dist/check-agent-guidance.js +2 -1
  18. package/dist/check-agent-guidance.js.map +1 -1
  19. package/dist/check-agent.d.ts.map +1 -1
  20. package/dist/check-agent.js +2 -1
  21. package/dist/check-agent.js.map +1 -1
  22. package/dist/doctor.js +3 -3
  23. package/dist/doctor.js.map +1 -1
  24. package/dist/help.js +1 -1
  25. package/dist/help.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +111 -86
  28. package/dist/index.js.map +1 -1
  29. package/dist/upgrade.js +1 -1
  30. package/dist/upgrade.js.map +1 -1
  31. package/package.json +4 -4
  32. package/templates/agent/rules/api-modules.mdc +1 -1
  33. package/templates/agent/rules/desktop.mdc +9 -0
  34. package/templates/agent/rules/security.mdc +9 -0
  35. package/templates/agent/skills/db-migration/SKILL.md +17 -0
  36. package/templates/agent/skills/desktop-electron/SKILL.md +18 -0
  37. package/templates/agent/skills/security-review/SKILL.md +20 -0
  38. package/templates/agent/skills/webhook-handling/SKILL.md +18 -0
  39. package/templates/agent/ysk-kit.mdc +1 -1
  40. package/templates/capabilities/billing/apps/api/src/modules/billing/application/billing-service.test.ts +56 -0
  41. package/templates/capabilities/billing/apps/api/src/modules/billing/application/billing-service.ts +71 -18
  42. package/templates/capabilities/billing/apps/api/src/modules/billing/domain/stripe-webhook-event.ts +12 -0
  43. package/templates/capabilities/billing/apps/api/src/modules/billing/domain/webhook-event-repository.ts +19 -0
  44. package/templates/capabilities/billing/apps/api/src/modules/billing/infra/billing.app.test.ts +172 -0
  45. package/templates/capabilities/billing/apps/api/src/modules/billing/infra/memory-webhook-event-repository.ts +31 -0
  46. package/templates/capabilities/billing/apps/api/src/modules/billing/infra/prisma-webhook-event-repository.ts +39 -0
  47. package/templates/capabilities/billing/apps/api/src/modules/billing/infra/stripe-billing.test.ts +3 -0
  48. package/templates/capabilities/billing/apps/api/src/modules/billing/infra/stripe-billing.ts +34 -7
  49. package/templates/capabilities/billing/packages/contracts/src/api/billing.ts +2 -2
  50. package/templates/capabilities/billing/packages/contracts/src/dto/billing.ts +1 -1
  51. package/templates/capabilities/billing/packages/sdk/src/resources/billing.ts +1 -1
  52. package/templates/capabilities/llm/apps/api/src/modules/llm/application/llm-service.test.ts +58 -0
  53. package/templates/capabilities/llm/apps/api/src/modules/llm/application/llm-service.ts +63 -36
  54. package/templates/capabilities/llm/apps/api/src/modules/llm/domain/usage-repository.ts +1 -0
  55. package/templates/capabilities/llm/apps/api/src/modules/llm/infra/llm.app.test.ts +49 -0
  56. package/templates/capabilities/llm/apps/api/src/modules/llm/infra/memory-usage-repository.ts +6 -3
  57. package/templates/capabilities/llm/apps/api/src/modules/llm/infra/prisma-usage-repository.ts +5 -0
  58. package/templates/capabilities/llm/packages/contracts/src/api/llm.ts +2 -2
  59. package/templates/capabilities/llm/packages/contracts/src/dto/llm.ts +8 -2
  60. package/templates/capabilities/llm/packages/sdk/src/resources/llm.ts +2 -2
  61. package/templates/capabilities/push/packages/contracts/src/api/devices.ts +2 -2
  62. package/templates/capabilities/push/packages/sdk/src/resources/devices.ts +1 -1
  63. package/templates/capabilities/team/packages/contracts/src/api/organizations.ts +2 -2
  64. package/templates/capabilities/team/packages/contracts/src/dto/organization.ts +1 -1
  65. package/templates/capabilities/team/packages/sdk/src/resources/organizations.ts +1 -1
  66. package/templates/module/api.ts.tmpl +3 -3
  67. package/templates/module/dto.ts.tmpl +1 -1
  68. package/templates/module/sdk-resource.ts.tmpl +1 -1
@@ -0,0 +1,58 @@
1
+ import { createFakeLlm, type ILlmPort, type LlmInput } from '@ysk-kit/llm';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { createMemoryLlmUsageRepository } from '../infra/memory-usage-repository';
4
+ import { createLlmService } from './llm-service';
5
+
6
+ const recordingLlm = (): { llm: ILlmPort; calls: LlmInput[] } => {
7
+ const calls: LlmInput[] = [];
8
+ const inner = createFakeLlm({ text: 'pong' });
9
+ return {
10
+ calls,
11
+ llm: {
12
+ model: inner.model,
13
+ complete: async (input) => {
14
+ calls.push(input);
15
+ return inner.complete(input);
16
+ },
17
+ stream: (input) => {
18
+ calls.push(input);
19
+ return inner.stream(input);
20
+ },
21
+ },
22
+ };
23
+ };
24
+
25
+ describe('createLlmService', () => {
26
+ it('prepends the server system prompt and rejects a client system role at the port', async () => {
27
+ const { llm, calls } = recordingLlm();
28
+ const usage = createMemoryLlmUsageRepository();
29
+ const service = createLlmService({
30
+ llm,
31
+ usage,
32
+ configured: true,
33
+ production: false,
34
+ systemPrompt: 'Server owned prompt',
35
+ });
36
+ await service.complete('user-1', { messages: [{ role: 'user', content: 'ping' }] });
37
+ expect(calls[0]?.messages[0]).toEqual({ role: 'system', content: 'Server owned prompt' });
38
+ expect(calls[0]?.messages[1]).toEqual({ role: 'user', content: 'ping' });
39
+ });
40
+
41
+ it('throws RATE_LIMITED when the per-user quota is exhausted', async () => {
42
+ const usage = createMemoryLlmUsageRepository();
43
+ const service = createLlmService({
44
+ llm: createFakeLlm({ text: 'pong' }),
45
+ usage,
46
+ configured: true,
47
+ production: false,
48
+ quota: { max: 1, windowMs: 60_000 },
49
+ });
50
+ await service.complete('user-1', { messages: [{ role: 'user', content: 'one' }] });
51
+ await expect(
52
+ service.complete('user-1', { messages: [{ role: 'user', content: 'two' }] }),
53
+ ).rejects.toMatchObject({ code: 'RATE_LIMITED' });
54
+ await expect(
55
+ service.complete('user-2', { messages: [{ role: 'user', content: 'ok' }] }),
56
+ ).resolves.toMatchObject({ text: 'pong' });
57
+ });
58
+ });
@@ -1,4 +1,5 @@
1
- import type { LlmCompleteCommand, LlmCompleteDto } from '@ysk-kit/contracts';
1
+ import { DEFAULT_LLM_SYSTEM_PROMPT } from '@ysk-kit/config';
2
+ import type { LlmCompleteCommand, LlmCompleteDto, LlmMessage } from '@ysk-kit/contracts';
2
3
  import { AppError } from '@ysk-kit/domain-kernel';
3
4
  import type { ILlmPort } from '@ysk-kit/llm';
4
5
  import type { ILlmUsageRepository } from '../domain/usage-repository';
@@ -8,41 +9,67 @@ export const createLlmService = (opts: {
8
9
  usage: ILlmUsageRepository;
9
10
  configured: boolean;
10
11
  production: boolean;
11
- }) => ({
12
- models: () => ({ items: [{ id: opts.llm.model }] }),
13
- complete: async (
14
- userId: string,
15
- body: LlmCompleteCommand,
16
- requestId?: string,
17
- ): Promise<LlmCompleteDto> => {
18
- if (opts.production && !opts.configured) {
19
- throw new AppError('INTERNAL', 'LLM not configured', 503);
20
- }
21
- const result = await opts.llm.complete(body);
22
- await opts.usage.create({
23
- userId,
24
- model: result.model,
25
- usage: result.usage,
26
- requestId,
27
- });
28
- return result;
29
- },
30
- stream: async function* (userId: string, body: LlmCompleteCommand, requestId?: string) {
31
- if (opts.production && !opts.configured) {
32
- throw new AppError('INTERNAL', 'LLM not configured', 503);
33
- }
34
- for await (const chunk of opts.llm.stream(body)) {
35
- if (chunk.done && chunk.usage) {
36
- await opts.usage.create({
37
- userId,
38
- model: opts.llm.model,
39
- usage: chunk.usage,
40
- requestId,
41
- });
12
+ systemPrompt?: string;
13
+ quota?: { max: number; windowMs: number };
14
+ }) => {
15
+ const systemPrompt =
16
+ opts.systemPrompt && opts.systemPrompt.length > 0
17
+ ? opts.systemPrompt
18
+ : DEFAULT_LLM_SYSTEM_PROMPT;
19
+
20
+ const withSystem = (body: LlmCompleteCommand) => {
21
+ const messages: LlmMessage[] = [{ role: 'system', content: systemPrompt }, ...body.messages];
22
+ return body.temperature === undefined
23
+ ? { messages }
24
+ : { messages, temperature: body.temperature };
25
+ };
26
+
27
+ const assertQuota = async (userId: string): Promise<void> => {
28
+ const quota = opts.quota;
29
+ if (!quota || quota.max === 0) return;
30
+ const since = new Date(Date.now() - quota.windowMs);
31
+ const used = await opts.usage.countSince(userId, since);
32
+ if (used >= quota.max) throw new AppError('RATE_LIMITED');
33
+ };
34
+
35
+ return {
36
+ models: () => ({ items: [{ id: opts.llm.model }] }),
37
+ complete: async (
38
+ userId: string,
39
+ body: LlmCompleteCommand,
40
+ requestId?: string,
41
+ ): Promise<LlmCompleteDto> => {
42
+ if (opts.production && !opts.configured) {
43
+ throw new AppError('INTERNAL', 'LLM not configured', 503);
44
+ }
45
+ await assertQuota(userId);
46
+ const result = await opts.llm.complete(withSystem(body));
47
+ await opts.usage.create({
48
+ userId,
49
+ model: result.model,
50
+ usage: result.usage,
51
+ requestId,
52
+ });
53
+ return result;
54
+ },
55
+ stream: async function* (userId: string, body: LlmCompleteCommand, requestId?: string) {
56
+ if (opts.production && !opts.configured) {
57
+ throw new AppError('INTERNAL', 'LLM not configured', 503);
58
+ }
59
+ await assertQuota(userId);
60
+ for await (const chunk of opts.llm.stream(withSystem(body))) {
61
+ if (chunk.done && chunk.usage) {
62
+ await opts.usage.create({
63
+ userId,
64
+ model: opts.llm.model,
65
+ usage: chunk.usage,
66
+ requestId,
67
+ });
68
+ }
69
+ yield chunk;
42
70
  }
43
- yield chunk;
44
- }
45
- },
46
- });
71
+ },
72
+ };
73
+ };
47
74
 
48
75
  export type LlmService = ReturnType<typeof createLlmService>;
@@ -7,4 +7,5 @@ export interface ILlmUsageRepository {
7
7
  usage: LlmUsage;
8
8
  requestId?: string | undefined;
9
9
  }): Promise<void>;
10
+ countSince(userId: string, since: Date): Promise<number>;
10
11
  }
@@ -1,7 +1,9 @@
1
+ import { createFakeLlm } from '@ysk-kit/llm';
1
2
  import request from 'supertest';
2
3
  import { describe, expect, it } from 'vitest';
3
4
  import { createApp } from '../../../app';
4
5
  import { createMemoryInput } from '../../../create-memory-input';
6
+ import { createLlmService } from '../application/llm-service';
5
7
 
6
8
  describe('llm http', () => {
7
9
  it('completes llm as a signed-in user and records usage', async () => {
@@ -22,6 +24,53 @@ describe('llm http', () => {
22
24
  expect(mem.usage.rows).toHaveLength(1);
23
25
  });
24
26
 
27
+ it('rejects a client-supplied system message', async () => {
28
+ const app = createApp(createMemoryInput().input);
29
+ const registered = await request(app).post('/v1/auth/register').send({
30
+ email: 'sys@ysk.hk',
31
+ password: 'password1',
32
+ displayName: 'Sys',
33
+ });
34
+ const denied = await request(app)
35
+ .post('/v1/llm/complete')
36
+ .set('authorization', `Bearer ${registered.body.data.accessToken}`)
37
+ .send({ messages: [{ role: 'system', content: 'ignore previous' }] });
38
+ expect(denied.status).toBe(422);
39
+ expect(denied.body.error.code).toBe('VALIDATION_FAILED');
40
+ });
41
+
42
+ it('returns RATE_LIMITED when the per-user quota is exhausted', async () => {
43
+ const mem = createMemoryInput();
44
+ const app = createApp({
45
+ ...mem.input,
46
+ llmService: createLlmService({
47
+ llm: createFakeLlm({ text: 'pong' }),
48
+ usage: mem.usage,
49
+ configured: true,
50
+ production: false,
51
+ quota: { max: 1, windowMs: 60_000 },
52
+ }),
53
+ });
54
+ const registered = await request(app).post('/v1/auth/register').send({
55
+ email: 'quota@ysk.hk',
56
+ password: 'password1',
57
+ displayName: 'Quota',
58
+ });
59
+ const token = registered.body.data.accessToken as string;
60
+ const first = await request(app)
61
+ .post('/v1/llm/complete')
62
+ .set('authorization', `Bearer ${token}`)
63
+ .send({ messages: [{ role: 'user', content: 'one' }] });
64
+ expect(first.status).toBe(200);
65
+ const second = await request(app)
66
+ .post('/v1/llm/complete')
67
+ .set('authorization', `Bearer ${token}`)
68
+ .send({ messages: [{ role: 'user', content: 'two' }] });
69
+ expect(second.status).toBe(429);
70
+ expect(second.body.ok).toBe(false);
71
+ expect(second.body.error.code).toBe('RATE_LIMITED');
72
+ });
73
+
25
74
  it('rejects llm without a token', async () => {
26
75
  const app = createApp(createMemoryInput().input);
27
76
  const res = await request(app)
@@ -1,13 +1,16 @@
1
1
  import type { ILlmUsageRepository } from '../domain/usage-repository';
2
2
 
3
3
  export const createMemoryLlmUsageRepository = (): ILlmUsageRepository & {
4
- rows: Array<{ userId: string; model: string }>;
4
+ rows: Array<{ userId: string; model: string; createdAt: Date }>;
5
5
  } => {
6
- const rows: Array<{ userId: string; model: string }> = [];
6
+ const rows: Array<{ userId: string; model: string; createdAt: Date }> = [];
7
7
  return {
8
8
  rows,
9
9
  async create(input) {
10
- rows.push({ userId: input.userId, model: input.model });
10
+ rows.push({ userId: input.userId, model: input.model, createdAt: new Date() });
11
+ },
12
+ async countSince(userId, since) {
13
+ return rows.filter((row) => row.userId === userId && row.createdAt >= since).length;
11
14
  },
12
15
  };
13
16
  };
@@ -14,4 +14,9 @@ export const createPrismaLlmUsageRepository = (prisma: PrismaClient): ILlmUsageR
14
14
  },
15
15
  });
16
16
  },
17
+ async countSince(userId, since) {
18
+ return prisma.llmUsage.count({
19
+ where: { userId, createdAt: { gte: since } },
20
+ });
21
+ },
17
22
  });
@@ -1,6 +1,6 @@
1
1
  import { initContract } from '@ts-rest/core';
2
- import { LlmCompleteCommandSchema, LlmCompleteDtoSchema, LlmModelsDtoSchema } from '../dto/llm';
3
- import { ErrSchema, OkSchema } from '../errors/envelope';
2
+ import { LlmCompleteCommandSchema, LlmCompleteDtoSchema, LlmModelsDtoSchema } from '../dto/llm.js';
3
+ import { ErrSchema, OkSchema } from '../errors/envelope.js';
4
4
 
5
5
  const c = initContract();
6
6
 
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { LlmRoleSchema } from '../enums/llm-role';
2
+ import { LlmClientRoleSchema, LlmRoleSchema } from '../enums/llm-role.js';
3
3
 
4
4
  export const LlmMessageSchema = z.object({
5
5
  role: LlmRoleSchema,
@@ -7,8 +7,14 @@ export const LlmMessageSchema = z.object({
7
7
  });
8
8
  export type LlmMessage = z.infer<typeof LlmMessageSchema>;
9
9
 
10
+ export const LlmClientMessageSchema = z.object({
11
+ role: LlmClientRoleSchema,
12
+ content: z.string().min(1).max(32_000),
13
+ });
14
+ export type LlmClientMessage = z.infer<typeof LlmClientMessageSchema>;
15
+
10
16
  export const LlmCompleteCommandSchema = z.object({
11
- messages: z.array(LlmMessageSchema).min(1).max(50),
17
+ messages: z.array(LlmClientMessageSchema).min(1).max(50),
12
18
  temperature: z.number().min(0).max(2).optional(),
13
19
  });
14
20
  export type LlmCompleteCommand = z.infer<typeof LlmCompleteCommandSchema>;
@@ -5,8 +5,8 @@ import type {
5
5
  LlmUsage,
6
6
  Platform,
7
7
  } from '@ysk-kit/contracts';
8
- import type { HttpClient } from '../http';
9
- import type { TokenStore } from '../token-store';
8
+ import type { HttpClient } from '../http.js';
9
+ import type { TokenStore } from '../token-store.js';
10
10
 
11
11
  export const llmResource = (
12
12
  http: HttpClient,
@@ -1,7 +1,7 @@
1
1
  import { initContract } from '@ts-rest/core';
2
2
  import { z } from 'zod';
3
- import { DeviceDtoSchema, RegisterDeviceCommandSchema } from '../dto/device';
4
- import { ErrSchema, OkSchema } from '../errors/envelope';
3
+ import { DeviceDtoSchema, RegisterDeviceCommandSchema } from '../dto/device.js';
4
+ import { ErrSchema, OkSchema } from '../errors/envelope.js';
5
5
 
6
6
  const c = initContract();
7
7
 
@@ -1,5 +1,5 @@
1
1
  import type { DeviceDto, RegisterDeviceCommand } from '@ysk-kit/contracts';
2
- import type { HttpClient } from '../http';
2
+ import type { HttpClient } from '../http.js';
3
3
 
4
4
  export const devicesResource = (http: HttpClient) => ({
5
5
  list: () => http.request<DeviceDto[]>('/v1/me/devices'),
@@ -9,8 +9,8 @@ import {
9
9
  OrganizationDtoSchema,
10
10
  OrgInviteDtoSchema,
11
11
  UpdateOrganizationCommandSchema,
12
- } from '../dto/organization';
13
- import { ErrSchema, OkSchema } from '../errors/envelope';
12
+ } from '../dto/organization.js';
13
+ import { ErrSchema, OkSchema } from '../errors/envelope.js';
14
14
 
15
15
  const c = initContract();
16
16
  const orgIdParams = z.object({ organizationId: z.string().uuid() });
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { InviteOrgRoleSchema, OrgRoleSchema } from '../enums/org-role';
2
+ import { InviteOrgRoleSchema, OrgRoleSchema } from '../enums/org-role.js';
3
3
 
4
4
  export const OrganizationDtoSchema = z.object({
5
5
  id: z.string().uuid(),
@@ -7,7 +7,7 @@ import type {
7
7
  OrgInviteDto,
8
8
  UpdateOrganizationCommand,
9
9
  } from '@ysk-kit/contracts';
10
- import type { HttpClient } from '../http';
10
+ import type { HttpClient } from '../http.js';
11
11
 
12
12
  export const organizationsResource = (http: HttpClient) => ({
13
13
  create: (body: CreateOrganizationCommand) =>
@@ -4,9 +4,9 @@ import {
4
4
  Create{{pascal}}CommandSchema,
5
5
  Paginated{{pascal}}Schema,
6
6
  {{pascal}}DtoSchema,
7
- } from '../dto/{{kebab}}';
8
- import { PageQuerySchema } from '../dto/user';
9
- import { ErrSchema, OkSchema } from '../errors/envelope';
7
+ } from '../dto/{{kebab}}.js';
8
+ import { PageQuerySchema } from '../dto/user.js';
9
+ import { ErrSchema, OkSchema } from '../errors/envelope.js';
10
10
 
11
11
  const c = initContract();
12
12
 
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { PaginatedSchema } from './user';
2
+ import { PaginatedSchema } from './user.js';
3
3
 
4
4
  export const {{pascal}}DtoSchema = z.object({
5
5
  id: z.string().uuid(),
@@ -1,5 +1,5 @@
1
1
  import type { Create{{pascal}}Command, Paginated{{pascal}}, {{pascal}}Dto } from '@ysk-kit/contracts';
2
- import type { HttpClient } from '../http';
2
+ import type { HttpClient } from '../http.js';
3
3
 
4
4
  const toQuery = (query?: { cursor?: string; limit?: number }): string => {
5
5
  const params = new URLSearchParams();