@dyyz1993/create-agent 2.0.0 → 2.1.0

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 (165) hide show
  1. package/package.json +1 -1
  2. package/src/commands/create.ts +31 -31
  3. package/src/commands/workspace.ts +112 -105
  4. package/src/lib/copy.ts +24 -1
  5. package/templates/agent/electron/main.js +46 -0
  6. package/templates/agent/electron/preload.js +5 -0
  7. package/templates/agent/electron-builder.json +40 -0
  8. package/templates/agent/eslint.config.mjs +17 -2
  9. package/templates/agent/package.json +69 -3
  10. package/templates/agent/src/__tests__/hybrid-mode.test.ts +126 -0
  11. package/templates/agent/src/__tests__/server-config-security.test.ts +55 -0
  12. package/templates/agent/src/bun/index.ts +96 -41
  13. package/templates/agent/src/bun/three.d.ts +1 -0
  14. package/templates/agent/src/gateway/__tests__/ws-handler-token.test.ts +118 -0
  15. package/templates/agent/src/gateway/http-routes.ts +1 -1
  16. package/templates/agent/src/gateway/ws-handler.ts +84 -56
  17. package/templates/agent/src/mainview/App.tsx +29 -26
  18. package/templates/agent/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  19. package/templates/agent/src/mainview/__tests__/setup.ts +32 -29
  20. package/templates/agent/src/mainview/__tests__/theme-variables.test.ts +36 -0
  21. package/templates/agent/src/mainview/components/chat/ChatPanel.tsx +88 -88
  22. package/templates/agent/src/mainview/components/common/ErrorBoundary.tsx +1 -58
  23. package/templates/agent/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  24. package/templates/agent/src/mainview/components/common/ThemeToggle.tsx +1 -0
  25. package/templates/agent/src/mainview/components/feed/FeedPanel.tsx +232 -210
  26. package/templates/agent/src/mainview/components/file-preview/VirtualizedCodeView.tsx +105 -81
  27. package/templates/agent/src/mainview/components/search/SearchPanel.tsx +427 -378
  28. package/templates/agent/src/mainview/components/todo/TodoPanel.tsx +149 -122
  29. package/templates/agent/src/mainview/hooks/use-input-history.ts +70 -61
  30. package/templates/agent/src/mainview/lib/api-client.ts +179 -172
  31. package/templates/agent/src/mainview/main.tsx +4 -10
  32. package/templates/agent/src/mainview/stores/use-feed-store.ts +107 -107
  33. package/templates/agent/src/mainview/utils/drop-handler.ts +114 -115
  34. package/templates/agent/src/server-config.ts +34 -32
  35. package/templates/agent/src/server.ts +42 -54
  36. package/templates/agent/src/shared/handlers/__tests__/chat-concurrency.test.ts +127 -0
  37. package/templates/agent/src/shared/handlers/__tests__/handler-isolation.test.ts +244 -0
  38. package/templates/agent/src/shared/handlers/bash.ts +65 -65
  39. package/templates/agent/src/shared/handlers/chat.ts +189 -155
  40. package/templates/agent/src/shared/handlers/debug.ts +12 -0
  41. package/templates/agent/src/shared/handlers/feed.ts +31 -32
  42. package/templates/agent/src/shared/handlers/git.ts +286 -243
  43. package/templates/agent/src/shared/handlers/index.ts +1 -0
  44. package/templates/agent/src/shared/handlers/rules.ts +31 -31
  45. package/templates/agent/src/shared/handlers/todo.ts +31 -31
  46. package/templates/agent/src/shared/http-routes.ts +250 -0
  47. package/templates/agent/src/shared/lib/__tests__/web-server.test.ts +190 -0
  48. package/templates/agent/src/shared/lib/bash-security.ts +43 -43
  49. package/templates/agent/src/shared/lib/logger.ts +117 -95
  50. package/templates/agent/src/shared/lib/web-server.ts +128 -0
  51. package/templates/agent/src/shared/modules/debug.ts +8 -0
  52. package/templates/agent/src/shared/rpc-schema.ts +16 -3
  53. package/templates/agent/test-upload.txt +0 -0
  54. package/templates/agent/tsconfig.ipc.json +14 -0
  55. package/templates/agent/tsconfig.json +13 -3
  56. package/templates/chat/eslint.config.mjs +15 -2
  57. package/templates/chat/package.json +13 -2
  58. package/templates/chat/src/__tests__/hybrid-mode.test.ts +125 -0
  59. package/templates/chat/src/__tests__/server-config-security.test.ts +55 -0
  60. package/templates/chat/src/__tests__/server-config.test.ts +46 -44
  61. package/templates/chat/src/bun/index.ts +94 -44
  62. package/templates/chat/src/bun/three.d.ts +1 -0
  63. package/templates/chat/src/gateway/http-routes.ts +1 -1
  64. package/templates/chat/src/gateway/ws-handler.ts +84 -56
  65. package/templates/chat/src/mainview/App.tsx +6 -0
  66. package/templates/chat/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  67. package/templates/chat/src/mainview/__tests__/theme-variables.test.ts +36 -0
  68. package/templates/chat/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  69. package/templates/chat/src/mainview/components/common/ThemeToggle.tsx +1 -0
  70. package/templates/chat/src/mainview/hooks/use-input-history.ts +70 -61
  71. package/templates/chat/src/mainview/lib/api-client.ts +180 -156
  72. package/templates/chat/src/mainview/lib/rpc-cache.ts +84 -0
  73. package/templates/chat/src/mainview/main.tsx +10 -6
  74. package/templates/chat/src/server-config.ts +33 -31
  75. package/templates/chat/src/server.ts +42 -60
  76. package/templates/chat/src/shared/handlers/chat.ts +196 -162
  77. package/templates/chat/src/shared/handlers/debug.ts +12 -0
  78. package/templates/chat/src/shared/handlers/index.ts +1 -0
  79. package/templates/chat/src/shared/http-routes.ts +250 -0
  80. package/templates/chat/src/shared/lib/__tests__/logger.test.ts +92 -0
  81. package/templates/chat/src/shared/lib/logger.ts +108 -95
  82. package/templates/chat/src/shared/lib/web-server.ts +128 -0
  83. package/templates/chat/src/shared/modules/debug.ts +8 -0
  84. package/templates/chat/src/shared/rpc-schema.ts +3 -2
  85. package/templates/chat/test-upload.txt +0 -0
  86. package/templates/chat/tsconfig.ipc.json +14 -0
  87. package/templates/chat/tsconfig.json +12 -2
  88. package/templates/general/eslint.config.mjs +15 -2
  89. package/templates/general/package.json +13 -2
  90. package/templates/general/src/__tests__/hybrid-mode.test.ts +125 -0
  91. package/templates/general/src/__tests__/server-config-security.test.ts +55 -0
  92. package/templates/general/src/__tests__/server-config.test.ts +46 -44
  93. package/templates/general/src/bun/index.ts +94 -44
  94. package/templates/general/src/bun/three.d.ts +1 -0
  95. package/templates/general/src/gateway/http-routes.ts +1 -1
  96. package/templates/general/src/gateway/ws-handler.ts +84 -56
  97. package/templates/general/src/mainview/__tests__/i18n/i18n.test.ts +1 -0
  98. package/templates/general/src/mainview/__tests__/theme-variables.test.ts +36 -0
  99. package/templates/general/src/mainview/components/common/LanguageSwitcher.tsx +1 -0
  100. package/templates/general/src/mainview/components/common/ThemeToggle.tsx +1 -0
  101. package/templates/general/src/mainview/components/feed/FeedPanel.tsx +241 -219
  102. package/templates/general/src/mainview/components/file-preview/VirtualizedCodeView.tsx +101 -81
  103. package/templates/general/src/mainview/components/layout/AppLayout.tsx +6 -0
  104. package/templates/general/src/mainview/components/search/SearchPanel.tsx +429 -378
  105. package/templates/general/src/mainview/hooks/use-input-history.ts +70 -61
  106. package/templates/general/src/mainview/lib/api-client.ts +180 -156
  107. package/templates/general/src/mainview/lib/rpc-cache.ts +84 -0
  108. package/templates/general/src/mainview/main.tsx +10 -6
  109. package/templates/general/src/mainview/stores/use-feed-store.ts +107 -107
  110. package/templates/general/src/mainview/utils/drop-handler.ts +114 -115
  111. package/templates/general/src/server-config.ts +33 -31
  112. package/templates/general/src/server.ts +42 -60
  113. package/templates/general/src/shared/handlers/__tests__/chat.test.ts +165 -160
  114. package/templates/general/src/shared/handlers/chat.ts +189 -155
  115. package/templates/general/src/shared/handlers/debug.ts +12 -0
  116. package/templates/general/src/shared/handlers/feed.ts +38 -40
  117. package/templates/general/src/shared/handlers/git.ts +286 -243
  118. package/templates/general/src/shared/handlers/index.ts +1 -0
  119. package/templates/general/src/shared/http-routes.ts +250 -0
  120. package/templates/general/src/shared/lib/__tests__/logger.test.ts +92 -0
  121. package/templates/general/src/shared/lib/logger.ts +117 -95
  122. package/templates/general/src/shared/lib/web-server.ts +128 -0
  123. package/templates/general/src/shared/modules/debug.ts +8 -0
  124. package/templates/general/src/shared/rpc-schema.ts +12 -2
  125. package/templates/general/test-upload.txt +0 -0
  126. package/templates/general/tsconfig.ipc.json +14 -0
  127. package/templates/general/tsconfig.json +12 -2
  128. package/templates/shared/__tests__/cors-security.test.ts +165 -0
  129. package/templates/shared/__tests__/file-path-security.test.ts +260 -0
  130. package/templates/shared/__tests__/http-routes.test.ts +174 -0
  131. package/templates/shared/__tests__/logger.test.ts +186 -0
  132. package/templates/shared/components/ErrorBoundary.tsx +59 -0
  133. package/templates/shared/env.d.ts +11 -0
  134. package/templates/shared/http-routes.ts +250 -0
  135. package/templates/shared/logger.ts +129 -0
  136. package/templates/shared/test-upload.txt +0 -0
  137. package/templates/shared/vite-base.config.ts +92 -0
  138. package/templates/shared/vitest-base.config.ts +42 -0
  139. package/templates/agent/eslint-plugin-rpc/index.js +0 -50
  140. package/templates/agent/eslint-plugin-rpc/package.json +0 -6
  141. package/templates/agent/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  142. package/templates/agent/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  143. package/templates/agent/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  144. package/templates/agent/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  145. package/templates/agent/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  146. package/templates/agent/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  147. package/templates/agent/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  148. package/templates/chat/eslint-plugin-rpc/index.js +0 -50
  149. package/templates/chat/eslint-plugin-rpc/package.json +0 -6
  150. package/templates/chat/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  151. package/templates/chat/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  152. package/templates/chat/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  153. package/templates/chat/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  154. package/templates/chat/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  155. package/templates/chat/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  156. package/templates/chat/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
  157. package/templates/general/eslint-plugin-rpc/index.js +0 -50
  158. package/templates/general/eslint-plugin-rpc/package.json +0 -6
  159. package/templates/general/eslint-plugin-rpc/rules/module-file-naming.js +0 -99
  160. package/templates/general/eslint-plugin-rpc/rules/no-bare-method.js +0 -78
  161. package/templates/general/eslint-plugin-rpc/rules/no-direct-register.js +0 -65
  162. package/templates/general/eslint-plugin-rpc/rules/no-hardcoded-strings.js +0 -69
  163. package/templates/general/eslint-plugin-rpc/rules/require-api-client.js +0 -58
  164. package/templates/general/eslint-plugin-rpc/rules/require-typed-register.js +0 -78
  165. package/templates/general/eslint-plugin-rpc/rules/schema-merge-only.js +0 -55
@@ -1,15 +1,7 @@
1
- /**
2
- * Web server entry point — HTTP file endpoints + WebSocket RPC gateway.
3
- * Port auto-negotiation: tries config.port first, increments on EADDRINUSE.
4
- * Writes actual port to .server-port for dev orchestration.
5
- */
6
-
7
- import { createServer } from "http";
8
1
  import { writeFileSync, unlinkSync, existsSync } from "fs";
9
2
  import { join, resolve, basename } from "path";
10
3
  import { config } from "./server-config";
11
- import { createHttpHandler } from "./gateway/http-routes";
12
- import { createWsHandler } from "./gateway/ws-handler";
4
+ import { createWebServer, getLocalIP } from "./shared/lib/web-server";
13
5
  import { createLogger, configureLogDir } from "./shared/lib/logger";
14
6
  import { registerPort, unregisterPort, formatRegistryForOutput } from "./shared/lib/port-registry";
15
7
  import { discoverMethodNames } from "./shared/register-all-handlers";
@@ -22,68 +14,58 @@ const PROJECT_ROOT = resolve(import.meta.dir, "..");
22
14
  const PROJECT_NAME = basename(PROJECT_ROOT);
23
15
 
24
16
  function cleanupPortFile() {
25
- try { if (existsSync(PORT_FILE)) unlinkSync(PORT_FILE); } catch {}
26
- unregisterPort(PROJECT_ROOT);
17
+ try {
18
+ if (existsSync(PORT_FILE)) unlinkSync(PORT_FILE);
19
+ } catch {}
20
+ unregisterPort(PROJECT_ROOT);
27
21
  }
28
22
 
29
23
  function writePortFile(port: number) {
30
- writeFileSync(PORT_FILE, String(port), "utf-8");
24
+ writeFileSync(PORT_FILE, String(port), "utf-8");
31
25
  }
32
26
 
33
27
  process.on("exit", cleanupPortFile);
34
- process.on("SIGINT", () => { cleanupPortFile(); process.exit(0); });
35
- process.on("SIGTERM", () => { cleanupPortFile(); process.exit(0); });
36
-
37
- const httpServer = createServer();
38
- const wss = createWsHandler(httpServer, { config });
28
+ process.on("SIGINT", () => {
29
+ cleanupPortFile();
30
+ process.exit(0);
31
+ });
32
+ process.on("SIGTERM", () => {
33
+ cleanupPortFile();
34
+ process.exit(0);
35
+ });
39
36
 
40
- httpServer.on("request", createHttpHandler({
41
- config,
42
- getWebSocketClientCount: () => wss.clients.size,
43
- }));
37
+ async function start() {
38
+ const { port, close } = await createWebServer({
39
+ port: config.port,
40
+ authToken: config.authToken,
41
+ maxUploadSize: config.maxUploadSize,
42
+ corsOrigin: config.corsOrigin,
43
+ });
44
44
 
45
- function checkPort(port: number): Promise<number> {
46
- return new Promise((resolve, reject) => {
47
- const testServer = createServer();
48
- testServer.once("error", (err: NodeJS.ErrnoException) => {
49
- testServer.close();
50
- reject(err);
51
- });
52
- testServer.once("listening", () => {
53
- testServer.close();
54
- resolve(port);
55
- });
56
- testServer.listen(port);
57
- });
58
- }
45
+ process.on("SIGINT", () => {
46
+ close();
47
+ cleanupPortFile();
48
+ process.exit(0);
49
+ });
50
+ process.on("SIGTERM", () => {
51
+ close();
52
+ cleanupPortFile();
53
+ process.exit(0);
54
+ });
59
55
 
60
- async function findAvailablePort(startPort: number, maxRetries: number = 10): Promise<number> {
61
- for (let port = startPort; port < startPort + maxRetries; port++) {
62
- try {
63
- await checkPort(port);
64
- return port;
65
- } catch {
66
- log.info(`Port ${port} in use, trying ${port + 1}...`);
67
- }
68
- }
69
- throw new Error(`No available port found after ${maxRetries} retries`);
70
- }
56
+ writePortFile(port);
57
+ registerPort(PROJECT_ROOT, port, PROJECT_NAME);
71
58
 
72
- async function start() {
73
- const port = await findAvailablePort(config.port);
74
- httpServer.listen(port, () => {
75
- writePortFile(port);
76
- registerPort(PROJECT_ROOT, port, PROJECT_NAME);
77
- log.info(`HTTP + WebSocket server running on http://localhost:${port}`);
78
- log.info(`WebSocket: ws://localhost:${port}/ws (auth required)`);
79
- log.info(`Available RPC methods: ${discoverMethodNames().join(", ")}`);
80
- log.info("File endpoints: GET /file/{path}, GET /info/{path}");
81
- // eslint-disable-next-line no-console
82
- console.log("\n" + formatRegistryForOutput() + "\n");
83
- });
59
+ const localIp = getLocalIP();
60
+ log.info(`HTTP + WebSocket server running on http://localhost:${port}`);
61
+ log.info(`Local network access: http://${localIp}:${port}`);
62
+ log.info(`WebSocket: ws://localhost:${port}/ws (auth required)`);
63
+ log.info(`Available RPC methods: ${discoverMethodNames().join(", ")}`);
64
+ log.info("File endpoints: GET /file/{path}, GET /info/{path}");
65
+ log.info(formatRegistryForOutput());
84
66
  }
85
67
 
86
68
  start().catch((err) => {
87
- log.error("Server failed to start", { error: err.message });
88
- process.exit(1);
69
+ log.error("Server failed to start", { error: err.message });
70
+ process.exit(1);
89
71
  });
@@ -9,175 +9,180 @@ import { homedir } from "os";
9
9
  type Handler = (params: unknown) => Promise<unknown>;
10
10
  type EmittedEvent = { event: string; payload: unknown; filter: unknown };
11
11
 
12
- const STORAGE_PATH = join(homedir(), ".pi-agent", "chat-history.json");
12
+ const STORAGE_PATH = join(homedir(), ".pi-agent", "chat-history-desktop.json");
13
13
 
14
14
  function createMockServer() {
15
- const handlers = new Map<string, Handler>();
16
- const events: EmittedEvent[] = [];
17
- return {
18
- register: (method: string, handler: Handler) => {
19
- handlers.set(method, handler);
20
- },
21
- emitEvent: (event: string, payload: unknown, filter?: unknown) => {
22
- events.push({ event, payload, filter });
23
- },
24
- get: (method: string) => handlers.get(method),
25
- has: (method: string) => handlers.has(method),
26
- getEvents: () => events,
27
- clearEvents: () => { events.length = 0; },
28
- };
15
+ const handlers = new Map<string, Handler>();
16
+ const events: EmittedEvent[] = [];
17
+ return {
18
+ register: (method: string, handler: Handler) => {
19
+ handlers.set(method, handler);
20
+ },
21
+ emitEvent: (event: string, payload: unknown, filter?: unknown) => {
22
+ events.push({ event, payload, filter });
23
+ },
24
+ get: (method: string) => handlers.get(method),
25
+ has: (method: string) => handlers.has(method),
26
+ getEvents: () => events,
27
+ clearEvents: () => {
28
+ events.length = 0;
29
+ },
30
+ };
29
31
  }
30
32
 
31
33
  async function clearHistory(): Promise<void> {
32
- try {
33
- if (existsSync(STORAGE_PATH)) {
34
- const { unlink } = await import("fs/promises");
35
- await unlink(STORAGE_PATH);
36
- }
37
- } catch {}
34
+ try {
35
+ if (existsSync(STORAGE_PATH)) {
36
+ const { unlink } = await import("fs/promises");
37
+ await unlink(STORAGE_PATH);
38
+ }
39
+ } catch {}
38
40
  }
39
41
 
40
42
  async function writeHistory(messages: Record<string, unknown>[]): Promise<void> {
41
- const dir = dirname(STORAGE_PATH);
42
- if (!existsSync(dir)) {
43
- await mkdir(dir, { recursive: true });
44
- }
45
- await writeFile(STORAGE_PATH, JSON.stringify(messages, null, 2), "utf-8");
43
+ const dir = dirname(STORAGE_PATH);
44
+ if (!existsSync(dir)) {
45
+ await mkdir(dir, { recursive: true });
46
+ }
47
+ await writeFile(STORAGE_PATH, JSON.stringify(messages, null, 2), "utf-8");
46
48
  }
47
49
 
48
50
  describe("chat handler", () => {
49
- let server: ReturnType<typeof createMockServer>;
50
-
51
- beforeEach(async () => {
52
- server = createMockServer();
53
- register(server as unknown as RPCServer, { platform: "desktop" });
54
- await clearHistory();
55
- });
56
-
57
- test("registers chat.list", () => {
58
- expect(server.has("chat.list")).toBe(true);
59
- });
60
-
61
- test("registers chat.send", () => {
62
- expect(server.has("chat.send")).toBe(true);
63
- });
64
-
65
- test("chat.list returns empty when no history", async () => {
66
- const result = await server.get("chat.list")!({}) as Record<string, unknown>;
67
- expect(result.messages).toEqual([]);
68
- expect(result.hasMore).toBe(false);
69
- });
70
-
71
- test("chat.list with existing history returns messages", async () => {
72
- const msgs = [
73
- { id: "1", role: "user", content: "hi", timestamp: 1000 },
74
- { id: "2", role: "assistant", content: "hello", timestamp: 2000 },
75
- ];
76
- await writeHistory(msgs);
77
-
78
- const result = await server.get("chat.list")!({}) as Record<string, unknown>;
79
- const messages = result.messages as Record<string, unknown>[];
80
- expect(messages.length).toBe(2);
81
- expect(result.hasMore).toBe(false);
82
- });
83
-
84
- test("chat.list respects limit param", async () => {
85
- const msgs = Array.from({ length: 100 }, (_, i) => ({
86
- id: `msg-${i}`,
87
- role: i % 2 === 0 ? "user" as const : "assistant" as const,
88
- content: `message ${i}`,
89
- timestamp: i * 1000,
90
- }));
91
- await writeHistory(msgs);
92
-
93
- const result = await server.get("chat.list")!({ limit: 10 }) as Record<string, unknown>;
94
- const messages = result.messages as Record<string, unknown>[];
95
- expect(messages.length).toBe(10);
96
- expect(result.hasMore).toBe(true);
97
- expect(messages[0].id).toBe("msg-90");
98
- });
99
-
100
- test("chat.list default limit is 50", async () => {
101
- const msgs = Array.from({ length: 60 }, (_, i) => ({
102
- id: `msg-${i}`,
103
- role: "user" as const,
104
- content: `message ${i}`,
105
- timestamp: i * 1000,
106
- }));
107
- await writeHistory(msgs);
108
-
109
- const result = await server.get("chat.list")!({}) as Record<string, unknown>;
110
- const messages = result.messages as Record<string, unknown>[];
111
- expect(messages.length).toBe(50);
112
- expect(result.hasMore).toBe(true);
113
- });
114
-
115
- test("chat.send returns { ok: true }", async () => {
116
- const result = await server.get("chat.send")!({ content: "hello" }) as Record<string, unknown>;
117
- expect(result.ok).toBe(true);
118
- });
119
-
120
- test("chat.send emits chat.message events for user and assistant", async () => {
121
- await server.get("chat.send")!({ content: "hi" });
122
-
123
- const events = server.getEvents().filter((e) => e.event === "chat.message");
124
- expect(events.length).toBe(2);
125
-
126
- const userEvent = events[0].payload as Record<string, unknown>;
127
- expect(userEvent.role).toBe("user");
128
- expect(userEvent.content).toBe("hi");
129
-
130
- const assistantEvent = events[1].payload as Record<string, unknown>;
131
- expect(assistantEvent.role).toBe("assistant");
132
- expect(assistantEvent.content).toBeTruthy();
133
- });
134
-
135
- test("chat.send stores messages in history", async () => {
136
- await server.get("chat.send")!({ content: "hello world" });
137
-
138
- const result = await server.get("chat.list")!({}) as Record<string, unknown>;
139
- const messages = result.messages as Record<string, unknown>[];
140
- expect(messages.length).toBe(2);
141
- expect(messages[0].role).toBe("user");
142
- expect(messages[0].content).toBe("hello world");
143
- expect(messages[1].role).toBe("assistant");
144
- });
145
-
146
- test("chat.send message has correct shape", async () => {
147
- await server.get("chat.send")!({ content: "test" });
148
-
149
- const events = server.getEvents().filter((e) => e.event === "chat.message");
150
- const userMsg = events[0].payload as Record<string, unknown>;
151
- expect(userMsg).toHaveProperty("id");
152
- expect(userMsg).toHaveProperty("role");
153
- expect(userMsg).toHaveProperty("content");
154
- expect(userMsg).toHaveProperty("timestamp");
155
- expect(typeof userMsg.id).toBe("string");
156
- expect(typeof userMsg.timestamp).toBe("number");
157
- });
158
-
159
- test("chat.message events include role filter", async () => {
160
- await server.get("chat.send")!({ content: "yo" });
161
-
162
- const events = server.getEvents().filter((e) => e.event === "chat.message");
163
- expect(events[0].filter).toEqual({ role: "user" });
164
- expect(events[1].filter).toEqual({ role: "assistant" });
165
- });
166
-
167
- test("chat.send with greeting gets greeting reply", async () => {
168
- await server.get("chat.send")!({ content: "hello" });
169
-
170
- const events = server.getEvents().filter((e) => e.event === "chat.message");
171
- const reply = events[1].payload as Record<string, unknown>;
172
- expect(reply.content).toBeTruthy();
173
- expect(reply.role).toBe("assistant");
174
- });
175
-
176
- test("chat.send with math expression gets calculation reply", async () => {
177
- await server.get("chat.send")!({ content: "12 * 8" });
178
-
179
- const events = server.getEvents().filter((e) => e.event === "chat.message");
180
- const reply = events[1].payload as Record<string, unknown>;
181
- expect(reply.content).toContain("96");
182
- });
51
+ let server: ReturnType<typeof createMockServer>;
52
+
53
+ beforeEach(async () => {
54
+ server = createMockServer();
55
+ register(server as unknown as RPCServer, { platform: "desktop" });
56
+ await clearHistory();
57
+ });
58
+
59
+ test("registers chat.list", () => {
60
+ expect(server.has("chat.list")).toBe(true);
61
+ });
62
+
63
+ test("registers chat.send", () => {
64
+ expect(server.has("chat.send")).toBe(true);
65
+ });
66
+
67
+ test("chat.list returns empty when no history", async () => {
68
+ const result = (await server.get("chat.list")!({})) as Record<string, unknown>;
69
+ expect(result.messages).toEqual([]);
70
+ expect(result.hasMore).toBe(false);
71
+ });
72
+
73
+ test("chat.list with existing history returns messages", async () => {
74
+ const msgs = [
75
+ { id: "1", role: "user", content: "hi", timestamp: 1000 },
76
+ { id: "2", role: "assistant", content: "hello", timestamp: 2000 },
77
+ ];
78
+ await writeHistory(msgs);
79
+
80
+ const result = (await server.get("chat.list")!({})) as Record<string, unknown>;
81
+ const messages = result.messages as Record<string, unknown>[];
82
+ expect(messages.length).toBe(2);
83
+ expect(result.hasMore).toBe(false);
84
+ });
85
+
86
+ test("chat.list respects limit param", async () => {
87
+ const msgs = Array.from({ length: 100 }, (_, i) => ({
88
+ id: `msg-${i}`,
89
+ role: i % 2 === 0 ? ("user" as const) : ("assistant" as const),
90
+ content: `message ${i}`,
91
+ timestamp: i * 1000,
92
+ }));
93
+ await writeHistory(msgs);
94
+
95
+ const result = (await server.get("chat.list")!({ limit: 10 })) as Record<string, unknown>;
96
+ const messages = result.messages as Record<string, unknown>[];
97
+ expect(messages.length).toBe(10);
98
+ expect(result.hasMore).toBe(true);
99
+ expect(messages[0].id).toBe("msg-90");
100
+ });
101
+
102
+ test("chat.list default limit is 50", async () => {
103
+ const msgs = Array.from({ length: 60 }, (_, i) => ({
104
+ id: `msg-${i}`,
105
+ role: "user" as const,
106
+ content: `message ${i}`,
107
+ timestamp: i * 1000,
108
+ }));
109
+ await writeHistory(msgs);
110
+
111
+ const result = (await server.get("chat.list")!({})) as Record<string, unknown>;
112
+ const messages = result.messages as Record<string, unknown>[];
113
+ expect(messages.length).toBe(50);
114
+ expect(result.hasMore).toBe(true);
115
+ });
116
+
117
+ test("chat.send returns { ok: true }", async () => {
118
+ const result = (await server.get("chat.send")!({ content: "hello" })) as Record<
119
+ string,
120
+ unknown
121
+ >;
122
+ expect(result.ok).toBe(true);
123
+ });
124
+
125
+ test("chat.send emits chat.message events for user and assistant", async () => {
126
+ await server.get("chat.send")!({ content: "hi" });
127
+
128
+ const events = server.getEvents().filter((e) => e.event === "chat.message");
129
+ expect(events.length).toBe(2);
130
+
131
+ const userEvent = events[0].payload as Record<string, unknown>;
132
+ expect(userEvent.role).toBe("user");
133
+ expect(userEvent.content).toBe("hi");
134
+
135
+ const assistantEvent = events[1].payload as Record<string, unknown>;
136
+ expect(assistantEvent.role).toBe("assistant");
137
+ expect(assistantEvent.content).toBeTruthy();
138
+ });
139
+
140
+ test("chat.send stores messages in history", async () => {
141
+ await server.get("chat.send")!({ content: "hello world" });
142
+
143
+ const result = (await server.get("chat.list")!({})) as Record<string, unknown>;
144
+ const messages = result.messages as Record<string, unknown>[];
145
+ expect(messages.length).toBe(2);
146
+ expect(messages[0].role).toBe("user");
147
+ expect(messages[0].content).toBe("hello world");
148
+ expect(messages[1].role).toBe("assistant");
149
+ });
150
+
151
+ test("chat.send message has correct shape", async () => {
152
+ await server.get("chat.send")!({ content: "test" });
153
+
154
+ const events = server.getEvents().filter((e) => e.event === "chat.message");
155
+ const userMsg = events[0].payload as Record<string, unknown>;
156
+ expect(userMsg).toHaveProperty("id");
157
+ expect(userMsg).toHaveProperty("role");
158
+ expect(userMsg).toHaveProperty("content");
159
+ expect(userMsg).toHaveProperty("timestamp");
160
+ expect(typeof userMsg.id).toBe("string");
161
+ expect(typeof userMsg.timestamp).toBe("number");
162
+ });
163
+
164
+ test("chat.message events include role filter", async () => {
165
+ await server.get("chat.send")!({ content: "yo" });
166
+
167
+ const events = server.getEvents().filter((e) => e.event === "chat.message");
168
+ expect(events[0].filter).toEqual({ role: "user" });
169
+ expect(events[1].filter).toEqual({ role: "assistant" });
170
+ });
171
+
172
+ test("chat.send with greeting gets greeting reply", async () => {
173
+ await server.get("chat.send")!({ content: "hello" });
174
+
175
+ const events = server.getEvents().filter((e) => e.event === "chat.message");
176
+ const reply = events[1].payload as Record<string, unknown>;
177
+ expect(reply.content).toBeTruthy();
178
+ expect(reply.role).toBe("assistant");
179
+ });
180
+
181
+ test("chat.send with math expression gets calculation reply", async () => {
182
+ await server.get("chat.send")!({ content: "12 * 8" });
183
+
184
+ const events = server.getEvents().filter((e) => e.event === "chat.message");
185
+ const reply = events[1].payload as Record<string, unknown>;
186
+ expect(reply.content).toContain("96");
187
+ });
183
188
  });