kiku-agent 1.0.11 → 1.2.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 (221) hide show
  1. package/README.md +300 -495
  2. package/package.json +12 -11
  3. package/public/fonts/GoogleSans-Bold.ttf +0 -0
  4. package/public/fonts/GoogleSans-Bold.woff2 +0 -0
  5. package/public/fonts/GoogleSans-BoldItalic.ttf +0 -0
  6. package/public/fonts/GoogleSans-Italic.ttf +0 -0
  7. package/public/fonts/GoogleSans-Latin-400.woff2 +0 -0
  8. package/public/fonts/GoogleSans-Latin-500.woff2 +0 -0
  9. package/public/fonts/GoogleSans-Latin-600.woff2 +0 -0
  10. package/public/fonts/GoogleSans-Latin-700.woff2 +0 -0
  11. package/public/fonts/GoogleSans-Medium.ttf +0 -0
  12. package/public/fonts/GoogleSans-Medium.woff2 +0 -0
  13. package/public/fonts/GoogleSans-Regular.ttf +0 -0
  14. package/public/fonts/GoogleSans-Regular.woff2 +0 -0
  15. package/public/fonts/GoogleSans-Thai-400.woff2 +0 -0
  16. package/public/fonts/GoogleSans-Thai-500.woff2 +0 -0
  17. package/public/fonts/GoogleSans-Thai-600.woff2 +0 -0
  18. package/public/fonts/GoogleSans-Thai-700.woff2 +0 -0
  19. package/public/fonts/NotoSansThai-Bold.woff2 +0 -0
  20. package/public/fonts/NotoSansThai-Regular.woff2 +0 -0
  21. package/public/fonts/google-sans-official.css +900 -0
  22. package/public/images/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  23. package/public/images/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  24. package/public/images/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  25. package/public/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  26. package/public/images/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  27. package/public/images/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  28. package/public/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  29. package/public/images/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  30. package/public/images/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  31. package/public/images/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  32. package/public/images/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  33. package/public/images/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  34. package/public/images/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  35. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-399915.jpg +0 -0
  36. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-424041.jpg +0 -0
  37. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-588166.jpg +0 -0
  38. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-672198.jpg +0 -0
  39. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-700522.jpg +0 -0
  40. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-839722.jpg +0 -0
  41. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026825-460586.jpg +0 -0
  42. package/public/images/douyin-update/thumbs//346/212/226/351/237/263202694-689407.jpg +0 -0
  43. package/public/images/douyin-update//346/212/226/351/237/2632026820-399915.mp4 +0 -0
  44. package/public/images/douyin-update//346/212/226/351/237/2632026820-424041.mp4 +0 -0
  45. package/public/images/douyin-update//346/212/226/351/237/2632026820-588166.mp4 +0 -0
  46. package/public/images/douyin-update//346/212/226/351/237/2632026820-672198.mp4 +0 -0
  47. package/public/images/douyin-update//346/212/226/351/237/2632026820-700522.mp4 +0 -0
  48. package/public/images/douyin-update//346/212/226/351/237/2632026820-839722.mp4 +0 -0
  49. package/public/images/douyin-update//346/212/226/351/237/2632026825-460586.mp4 +0 -0
  50. package/public/images/douyin-update//346/212/226/351/237/263202694-689407.mp4 +0 -0
  51. package/public/images/general/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  52. package/public/images/general/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  53. package/public/images/general/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  54. package/public/images/general/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  55. package/public/images/general/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  56. package/public/images/general/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  57. package/public/images/general/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  58. package/public/images/general/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  59. package/public/images/general/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  60. package/public/images/general/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  61. package/public/images/general/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  62. package/public/images/general/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  63. package/public/images/general/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  64. package/public/images/general/jujingyi_portrait.jpg +0 -0
  65. package/public/images/jujingyi_portrait.jpg +0 -0
  66. package/public/images/series/a-millenniums-wait/poster.jpg +0 -0
  67. package/public/images/series/beauty-of-resilience/poster.jpg +0 -0
  68. package/public/images/series/detective-samoyeds/poster.jpg +0 -0
  69. package/public/images/series/everyday/poster.jpg +0 -0
  70. package/public/images/series/fu-rong-ancient-painting/poster.jpg +0 -0
  71. package/public/images/series/in-a-class-of-her-own/poster.jpg +0 -0
  72. package/public/images/series/in-blossom/poster.jpg +0 -0
  73. package/public/images/series/legend-of-yunxi/poster.jpg +0 -0
  74. package/public/images/series/lets-fight/poster-2.jpg +0 -0
  75. package/public/images/series/lets-fight/poster.jpg +0 -0
  76. package/public/images/series/love-emergency/poster.jpg +0 -0
  77. package/public/images/series/love-under-the-full-moon/poster.jpg +0 -0
  78. package/public/images/series/money-bastion/poster.jpg +0 -0
  79. package/public/images/series/mr-swimmer/poster.jpg +0 -0
  80. package/public/images/series/novoland-castle-in-the-sky/poster.jpg +0 -0
  81. package/public/images/series/please-give-me-a-pair-of-wings/poster.jpg +0 -0
  82. package/public/images/series/rebirth-for-you/poster.jpg +0 -0
  83. package/public/images/series/sword-and-fairy-4/poster.jpg +0 -0
  84. package/public/images/series/the-blooms-at-ruyi-pavilion/poster.jpg +0 -0
  85. package/public/images/series/the-eternal-fragrance/poster-2.jpg +0 -0
  86. package/public/images/series/the-eternal-fragrance/poster.jpg +0 -0
  87. package/public/images/series/the-fallen-flowers/poster.jpg +0 -0
  88. package/public/images/series/the-legend-of-white-snake/poster.jpg +0 -0
  89. package/public/images/series/veil-of-shadows/poster.jpg +0 -0
  90. package/public/images/series/wanhua-sin/poster-2.jpg +0 -0
  91. package/public/images/series/wanhua-sin/poster.jpg +0 -0
  92. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj10yogj62u03s0npe02.jpg +0 -0
  93. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj2pdvyj61li25jkjl02.jpg +0 -0
  94. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj71177j62672w9e8302.jpg +0 -0
  95. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj7twpfj62192197rw02.jpg +0 -0
  96. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj8jfu8j62c03401kx02.jpg +0 -0
  97. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj9dhdsj622k2l77wh02.jpg +0 -0
  98. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkja31s4j62bw3407r802.jpg +0 -0
  99. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjcf331j61402ey1kx02.jpg +0 -0
  100. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjdx8gqj62602pihdt02.jpg +0 -0
  101. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjfvxnzj62432n57wh02.jpg +0 -0
  102. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixniv2yj627y4fxb2b02.jpg +0 -0
  103. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixsvbdaj625k2vfu1102.jpg +0 -0
  104. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixxxdaoj62c03501l202.jpg +0 -0
  105. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixzeef9j62c0340npd02.jpg +0 -0
  106. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy0vs6tj626t4rbhdu02.jpg +0 -0
  107. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy21wwuj62bt34hx6q02.jpg +0 -0
  108. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly4wr85j62z629y7wh02.jpg +0 -0
  109. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly67oilj623u2t5npd02.jpg +0 -0
  110. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly9qtw8j62c0340u0x02.jpg +0 -0
  111. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyem4m4j63402c01kz02.jpg +0 -0
  112. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlygkoq4j627s2yeavd02.jpg +0 -0
  113. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyjh0daj62bw340nm102.jpg +0 -0
  114. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlylejlhj62bw3401kx02.jpg +0 -0
  115. package/public/images/weibo-update/today/0040jbadgy1igaiaa8b7fj627j2y67wh02.jpg +0 -0
  116. package/public/images/weibo-update/today/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
  117. package/public/images/weibo-update/today/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
  118. package/public/images/weibo-update/today/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
  119. package/public/images/weibo-update/today/0040jbadgy1igaiapyartj62bw340hdt02.jpg +0 -0
  120. package/public/images/weibo-update/today/0040jbadgy1igaibkwu4ij61vy2iq1kx02.jpg +0 -0
  121. package/public/images/weibo-update/today/0040jbadgy1igaic1fvkpj61u92eo1ky02.jpg +0 -0
  122. package/public/images/weibo-update/today/0040jbadgy1igaic3cruhj62822ywb2902.jpg +0 -0
  123. package/public/manifest.json +25 -0
  124. package/public/mcp-logo-128.png +0 -0
  125. package/public/mcp-logo-64.png +0 -0
  126. package/public/mcp-logo.svg +16 -0
  127. package/server/agent/agentCore.js +542 -74
  128. package/server/agent/appPaths.js +1 -0
  129. package/server/agent/audioPlayer.js +4 -2
  130. package/server/agent/citations.js +294 -0
  131. package/server/agent/clarificationAnalytics.js +477 -0
  132. package/server/agent/clarifyingQuestions.js +1089 -0
  133. package/server/agent/cli.js +549 -125
  134. package/server/agent/commandFeatures.js +1 -0
  135. package/server/agent/commandRegistry.js +946 -879
  136. package/server/agent/commands.js +546 -272
  137. package/server/agent/config.js +581 -412
  138. package/server/agent/contextCompactor.js +2 -2
  139. package/server/agent/debate.js +235 -0
  140. package/server/agent/eventBus.js +3 -0
  141. package/server/agent/feedbackLearning.js +217 -0
  142. package/server/agent/llmClient.js +512 -394
  143. package/server/agent/mcpClient.js +182 -32
  144. package/server/agent/memory.js +283 -224
  145. package/server/agent/modelManager.js +79 -39
  146. package/server/agent/profiles.js +178 -0
  147. package/server/agent/publicTools.js +22 -0
  148. package/server/agent/queryDetails.js +68 -8
  149. package/server/agent/quizData.js +398 -384
  150. package/server/agent/research.js +121 -79
  151. package/server/agent/sanitizer.js +104 -13
  152. package/server/agent/session.js +319 -11
  153. package/server/agent/subagent.js +8 -3
  154. package/server/agent/suggestionTui.js +294 -0
  155. package/server/agent/suggestions.js +69 -3
  156. package/server/agent/telemetry.js +119 -0
  157. package/server/agent/terminalImage.js +146 -27
  158. package/server/agent/theme.js +17 -0
  159. package/server/agent/toolRouter.js +16 -1
  160. package/server/agent/tools.js +424 -225
  161. package/server/agent/ui.js +486 -64
  162. package/server/agent/version.js +2 -4
  163. package/server/agent/webSearch.js +111 -11
  164. package/server/agent/youtubeAuth.js +24 -6
  165. package/server/apiRouter.js +4222 -0
  166. package/server/mcp/data/discography.js +25 -16
  167. package/server/mcp/data/gallery.js +308 -20
  168. package/server/mcp/data/profile.js +74 -44
  169. package/server/mcp/data/timeline.js +1 -1
  170. package/server/mcp/data/works.js +51 -17
  171. package/server/mcp/dataAdapter.js +5 -3
  172. package/server/mcp/handler.js +5 -11
  173. package/server/mcp/prompts.js +86 -0
  174. package/server/mcp/security.js +18 -15
  175. package/server/mcp/server.js +7 -2
  176. package/server/mcp/tools.js +66 -2
  177. package/server/web-chat/core/browserLauncher.js +18 -0
  178. package/server/web-chat/core/connection.js +194 -0
  179. package/server/web-chat/core/conversationContext.js +82 -0
  180. package/server/web-chat/core/directCatalogEngine.js +196 -0
  181. package/server/web-chat/core/dramaKnowledge.js +142 -0
  182. package/server/web-chat/core/errorHandler.js +51 -0
  183. package/server/web-chat/core/followUpEngine.js +182 -0
  184. package/server/web-chat/core/futureEventGuard.js +80 -0
  185. package/server/web-chat/core/memorySnapshot.js +12 -0
  186. package/server/web-chat/core/security.js +146 -0
  187. package/server/web-chat/core/toolInspector.js +52 -0
  188. package/server/web-chat/router.js +120 -0
  189. package/server/web-chat/routes/chatRoute.js +397 -0
  190. package/server/web-chat/routes/clarifyConfigRoute.js +98 -0
  191. package/server/web-chat/routes/clarifyRoute.js +83 -0
  192. package/server/web-chat/routes/compareRoute.js +79 -0
  193. package/server/web-chat/routes/exportRoute.js +59 -0
  194. package/server/web-chat/routes/feedbackRoute.js +57 -0
  195. package/server/web-chat/routes/newsRoute.js +33 -0
  196. package/server/web-chat/routes/quizRoute.js +11 -0
  197. package/server/web-chat/routes/staticRoute.js +103 -0
  198. package/server/web-chat/routes/statusRoute.js +52 -0
  199. package/server/web-chat/routes/suggestRoute.js +77 -0
  200. package/server/web-chat/routes/timelineRoute.js +34 -0
  201. package/server/web-chat/routes/verifyRoute.js +57 -0
  202. package/server/web-chat/server.js +116 -0
  203. package/server/web-chat/ui/htmlBuilder.js +91 -0
  204. package/server/web-chat/ui/previews/agentBubble.preview.html +134 -0
  205. package/server/web-chat/ui/scripts/agentBubbleSystem.js +320 -0
  206. package/server/web-chat/ui/scripts/client.js +8397 -0
  207. package/server/web-chat/ui/scripts/kikuDb.js +177 -0
  208. package/server/web-chat/ui/scripts/suggestionsData.js +118 -0
  209. package/server/web-chat/ui/scripts/webchatI18n.js +291 -0
  210. package/server/web-chat/ui/styles/main.css +8568 -0
  211. package/server/web-chat/ui/templates/body.html +796 -0
  212. package/server/webChatHtml.js +1 -0
  213. package/server/webChatServer.js +6 -0
  214. package/src/data/discographyData.js +892 -866
  215. package/src/data/galleryData.js +1805 -561
  216. package/src/data/profileData.js +79 -2
  217. package/src/data/socialUpdatesData.js +106 -47
  218. package/src/data/timelineData.js +1 -1
  219. package/src/data/worksData.js +25 -4
  220. package/tokens.css +97 -0
  221. package/server/agent/updateManager.js +0 -456
@@ -1,456 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { existsSync, promises as fs } from 'node:fs';
3
- import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { KIKU_DATA_DIR } from './appPaths.js';
6
- import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
7
-
8
- export const UPDATE_CHANNELS = Object.freeze(['latest', 'next', 'canary']);
9
- export const UPDATE_STATE_FILE = path.join(KIKU_DATA_DIR, 'update-state.json');
10
-
11
- const PACKAGE_ROOT = fileURLToPath(new URL('../../', import.meta.url));
12
- const STRICT_SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
13
- const MAX_NPM_OUTPUT = 2 * 1024 * 1024;
14
- const DEFAULT_TIMEOUT_MS = 20_000;
15
- const INSTALL_TIMEOUT_MS = 180_000;
16
-
17
- export class UpdateError extends Error {
18
- constructor(message, code = 'UPDATE_ERROR', details = {}) {
19
- super(message);
20
- this.name = 'UpdateError';
21
- this.code = code;
22
- this.details = details;
23
- }
24
- }
25
-
26
- function cleanOutput(value, maxLength = 400) {
27
- const text = String(value || '')
28
- .replace(/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g, '')
29
- .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
30
- .split('\n')
31
- .map(line => line.replace(/^npm\s+(?:error|ERR!)\s*/i, '').trim())
32
- .filter(Boolean)
33
- .join(' ');
34
- return text.slice(0, maxLength);
35
- }
36
-
37
- function parseSemver(value) {
38
- const normalized = String(value || '').trim().replace(/^v/, '');
39
- const match = normalized.match(STRICT_SEMVER);
40
- if (!match) return null;
41
- return {
42
- raw: normalized,
43
- major: Number(match[1]),
44
- minor: Number(match[2]),
45
- patch: Number(match[3]),
46
- prerelease: match[4] ? match[4].split('.') : [],
47
- };
48
- }
49
-
50
- function compareIdentifiers(left, right) {
51
- const leftNumeric = /^\d+$/.test(left);
52
- const rightNumeric = /^\d+$/.test(right);
53
- if (leftNumeric && rightNumeric) return Number(left) - Number(right);
54
- if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
55
- return left.localeCompare(right);
56
- }
57
-
58
- export function compareVersions(left, right) {
59
- const a = parseSemver(left);
60
- const b = parseSemver(right);
61
- if (!a || !b) throw new UpdateError('The registry returned an invalid semantic version.', 'INVALID_SEMVER');
62
- for (const key of ['major', 'minor', 'patch']) {
63
- if (a[key] !== b[key]) return Math.sign(a[key] - b[key]);
64
- }
65
- if (a.prerelease.length === 0 && b.prerelease.length === 0) return 0;
66
- if (a.prerelease.length === 0) return 1;
67
- if (b.prerelease.length === 0) return -1;
68
- const max = Math.max(a.prerelease.length, b.prerelease.length);
69
- for (let index = 0; index < max; index++) {
70
- if (a.prerelease[index] === undefined) return -1;
71
- if (b.prerelease[index] === undefined) return 1;
72
- const compared = compareIdentifiers(a.prerelease[index], b.prerelease[index]);
73
- if (compared !== 0) return Math.sign(compared);
74
- }
75
- return 0;
76
- }
77
-
78
- export function isValidUpdateTarget(value) {
79
- const normalized = String(value || '').trim().toLowerCase();
80
- return UPDATE_CHANNELS.includes(normalized) || Boolean(parseSemver(normalized));
81
- }
82
-
83
- export function normalizeUpdateTarget(value, fallback = 'latest') {
84
- const normalized = String(value || fallback).trim().toLowerCase().replace(/^v(?=\d)/, '');
85
- if (!isValidUpdateTarget(normalized)) {
86
- throw new UpdateError(
87
- 'Invalid update target. Use latest, next, canary, or an exact version such as 1.2.3.',
88
- 'INVALID_TARGET'
89
- );
90
- }
91
- return normalized;
92
- }
93
-
94
- function satisfiesComparator(version, comparator) {
95
- const match = comparator.match(/^(>=|<=|>|<|=|\^|~)?\s*(v?\d+(?:\.\d+){0,2})(?:\.x)?$/i);
96
- if (!match) return false;
97
- const operator = match[1] || '=';
98
- const numericParts = match[2].replace(/^v/, '').split('.').map(Number);
99
- while (numericParts.length < 3) numericParts.push(0);
100
- const target = numericParts.join('.');
101
- const compared = compareVersions(version, target);
102
- if (operator === '>=') return compared >= 0;
103
- if (operator === '<=') return compared <= 0;
104
- if (operator === '>') return compared > 0;
105
- if (operator === '<') return compared < 0;
106
- if (operator === '~') {
107
- const parsed = parseSemver(version);
108
- return compared >= 0 && parsed.major === numericParts[0] && parsed.minor === numericParts[1];
109
- }
110
- if (operator === '^') {
111
- const parsed = parseSemver(version);
112
- if (numericParts[0] > 0) return compared >= 0 && parsed.major === numericParts[0];
113
- if (numericParts[1] > 0) return compared >= 0 && parsed.major === 0 && parsed.minor === numericParts[1];
114
- return compared >= 0 && parsed.major === 0 && parsed.minor === 0 && parsed.patch === numericParts[2];
115
- }
116
- return compared === 0;
117
- }
118
-
119
- export function satisfiesNodeEngine(nodeVersion, range) {
120
- if (!range || range === '*') return true;
121
- const version = String(nodeVersion || '').replace(/^v/, '').split('-')[0];
122
- if (!parseSemver(version)) return false;
123
- return String(range).split('||').some(alternative => {
124
- const comparators = alternative.trim().split(/\s+/).filter(Boolean);
125
- return comparators.length > 0 && comparators.every(comparator => satisfiesComparator(version, comparator));
126
- });
127
- }
128
-
129
- function defaultNpmInvocation() {
130
- const nodeDir = path.dirname(process.execPath);
131
- const candidates = [
132
- path.join(nodeDir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
133
- path.resolve(nodeDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
134
- process.env.npm_execpath,
135
- ].filter(Boolean);
136
- const npmCli = candidates.find(candidate => /npm-cli\.(?:c?js|mjs)$/i.test(candidate) && existsSync(candidate));
137
- if (npmCli) return { command: process.execPath, prefixArgs: [npmCli] };
138
- return { command: process.platform === 'win32' ? 'npm.cmd' : 'npm', prefixArgs: [] };
139
- }
140
-
141
- export function runNpm(args, options = {}) {
142
- if (!Array.isArray(args) || args.some(arg => typeof arg !== 'string')) {
143
- return Promise.reject(new UpdateError('Invalid npm arguments.', 'INVALID_NPM_ARGS'));
144
- }
145
- const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
146
- const invocation = options.invocation || defaultNpmInvocation();
147
- return new Promise((resolve, reject) => {
148
- let stdout = '';
149
- let stderr = '';
150
- let settled = false;
151
- const child = spawn(invocation.command, [...invocation.prefixArgs, ...args], {
152
- shell: false,
153
- windowsHide: true,
154
- stdio: ['ignore', 'pipe', 'pipe'],
155
- env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },
156
- });
157
- const finish = (error, result) => {
158
- if (settled) return;
159
- settled = true;
160
- clearTimeout(timer);
161
- if (error) reject(error);
162
- else resolve(result);
163
- };
164
- const append = (stream, chunk) => {
165
- const next = stream + chunk.toString('utf8');
166
- if (next.length > MAX_NPM_OUTPUT) {
167
- try { child.kill('SIGKILL'); } catch {}
168
- finish(new UpdateError('npm returned more data than expected.', 'NPM_OUTPUT_LIMIT'));
169
- return stream;
170
- }
171
- return next;
172
- };
173
- child.stdout.on('data', chunk => { stdout = append(stdout, chunk); });
174
- child.stderr.on('data', chunk => {
175
- stderr = append(stderr, chunk);
176
- if (typeof options.onStderr === 'function') options.onStderr(cleanOutput(chunk, 1000));
177
- });
178
- child.once('error', error => finish(new UpdateError(
179
- `Unable to run npm: ${cleanOutput(error.message)}`,
180
- 'NPM_UNAVAILABLE',
181
- { cause: error.code }
182
- )));
183
- child.once('close', code => finish(null, { code, stdout: stdout.trim(), stderr: stderr.trim() }));
184
- const timer = setTimeout(() => {
185
- try { child.kill('SIGKILL'); } catch {}
186
- finish(new UpdateError('npm did not respond before the timeout.', 'NPM_TIMEOUT'));
187
- }, timeoutMs);
188
- });
189
- }
190
-
191
- function safeJsonParse(raw, code) {
192
- try {
193
- return JSON.parse(raw);
194
- } catch {
195
- throw new UpdateError('npm returned an unreadable response.', code);
196
- }
197
- }
198
-
199
- function isPathInside(candidate, parent) {
200
- const relative = path.relative(path.resolve(parent), path.resolve(candidate));
201
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
202
- }
203
-
204
- function normalizeState(raw = {}) {
205
- const channel = UPDATE_CHANNELS.includes(raw.channel) ? raw.channel : 'latest';
206
- const installs = Array.isArray(raw.installs)
207
- ? raw.installs.filter(item => parseSemver(item?.from) && parseSemver(item?.to)).slice(-20)
208
- : [];
209
- return { schemaVersion: 1, channel, installs, lastCheck: raw.lastCheck || null };
210
- }
211
-
212
- export class UpdateManager {
213
- constructor(options = {}) {
214
- this.packageName = options.packageName || PACKAGE_NAME;
215
- this.currentVersion = options.currentVersion || PACKAGE_VERSION;
216
- this.packageRoot = options.packageRoot || PACKAGE_ROOT;
217
- this.stateFile = options.stateFile || UPDATE_STATE_FILE;
218
- this.nodeVersion = options.nodeVersion || process.versions.node;
219
- this.env = options.env || process.env;
220
- this.runner = options.runner || ((args, runnerOptions) => runNpm(args, runnerOptions));
221
- }
222
-
223
- async readState() {
224
- try {
225
- return normalizeState(JSON.parse(await fs.readFile(this.stateFile, 'utf8')));
226
- } catch (error) {
227
- if (error.code === 'ENOENT' || error instanceof SyntaxError) return normalizeState();
228
- throw new UpdateError('Unable to read update settings.', 'STATE_READ_FAILED', { cause: error.code });
229
- }
230
- }
231
-
232
- async writeState(state) {
233
- const normalized = normalizeState(state);
234
- const directory = path.dirname(this.stateFile);
235
- const temporary = path.join(directory, `.update-state-${process.pid}-${Date.now()}.tmp`);
236
- try {
237
- await fs.mkdir(directory, { recursive: true, mode: 0o700 });
238
- await fs.writeFile(temporary, JSON.stringify(normalized, null, 2), { encoding: 'utf8', mode: 0o600 });
239
- await fs.rename(temporary, this.stateFile);
240
- } catch (error) {
241
- try { await fs.unlink(temporary); } catch {}
242
- throw new UpdateError('Unable to save update settings.', 'STATE_WRITE_FAILED', { cause: error.code });
243
- }
244
- return normalized;
245
- }
246
-
247
- async getChannel() {
248
- return (await this.readState()).channel;
249
- }
250
-
251
- async setChannel(channel) {
252
- const normalized = normalizeUpdateTarget(channel);
253
- if (!UPDATE_CHANNELS.includes(normalized)) {
254
- throw new UpdateError('Update channels are latest, next, or canary.', 'INVALID_CHANNEL');
255
- }
256
- const state = await this.readState();
257
- state.channel = normalized;
258
- await this.writeState(state);
259
- return normalized;
260
- }
261
-
262
- async detectInstallMode() {
263
- const normalizedRoot = path.resolve(this.packageRoot);
264
- if (/[\\/]_npx[\\/]/i.test(normalizedRoot) || this.env.npm_command === 'exec') {
265
- return { id: 'npx', label: 'npx / npm exec', automatic: false };
266
- }
267
- if (existsSync(path.join(normalizedRoot, '.git'))) {
268
- return { id: 'source', label: 'Source checkout', automatic: false };
269
- }
270
- try {
271
- const result = await this.runner(['root', '--global'], { timeoutMs: 5_000 });
272
- if (result.code === 0 && result.stdout && isPathInside(normalizedRoot, result.stdout.trim())) {
273
- return { id: 'global', label: 'Global npm', automatic: true };
274
- }
275
- } catch {}
276
- if (/[\\/]node_modules[\\/]/i.test(normalizedRoot)) {
277
- return { id: 'local', label: 'Local project dependency', automatic: false };
278
- }
279
- return { id: 'unknown', label: 'Unknown/manual installation', automatic: false };
280
- }
281
-
282
- async fetchRelease(target) {
283
- const normalizedTarget = normalizeUpdateTarget(target);
284
- const spec = `${this.packageName}@${normalizedTarget}`;
285
- const result = await this.runner(['view', spec, '--json', '--prefer-online'], { timeoutMs: DEFAULT_TIMEOUT_MS });
286
- if (result.code !== 0) {
287
- const errText = String(result.stderr || '');
288
- if (/E404|404\s+No match found|No match found for version|not found|404\s+Not\s+Found/i.test(errText)) {
289
- if (UPDATE_CHANNELS.includes(normalizedTarget)) {
290
- throw new UpdateError(
291
- `No release is currently available for channel "${normalizedTarget}" on npm. (Only "latest" is currently published).`,
292
- 'CHANNEL_NOT_FOUND',
293
- { target: normalizedTarget, exitCode: result.code }
294
- );
295
- }
296
- throw new UpdateError(
297
- `Version or tag "${normalizedTarget}" was not found on the npm registry.`,
298
- 'RELEASE_NOT_FOUND',
299
- { target: normalizedTarget, exitCode: result.code }
300
- );
301
- }
302
- throw new UpdateError(
303
- cleanOutput(result.stderr) || 'npm could not find the requested release.',
304
- 'REGISTRY_LOOKUP_FAILED',
305
- { exitCode: result.code }
306
- );
307
- }
308
- const manifest = safeJsonParse(result.stdout, 'REGISTRY_RESPONSE_INVALID');
309
- if (manifest.name !== this.packageName || !parseSemver(manifest.version)) {
310
- throw new UpdateError('The registry response did not match kiku-agent.', 'REGISTRY_IDENTITY_MISMATCH');
311
- }
312
- if (parseSemver(normalizedTarget) && compareVersions(manifest.version, normalizedTarget) !== 0) {
313
- throw new UpdateError('The registry resolved a different version than requested.', 'REGISTRY_VERSION_MISMATCH');
314
- }
315
- const metadata = manifest.kikuUpdate && typeof manifest.kikuUpdate === 'object' ? manifest.kikuUpdate : {};
316
- const rawNotes = metadata.releaseSummary || metadata.releaseNotes || [];
317
- const releaseNotes = (Array.isArray(rawNotes) ? rawNotes : [rawNotes])
318
- .map(note => cleanOutput(note, 240))
319
- .filter(Boolean)
320
- .slice(0, 10);
321
- return {
322
- target: normalizedTarget,
323
- version: manifest.version,
324
- engines: manifest.engines || {},
325
- deprecated: cleanOutput(manifest.deprecated, 300) || null,
326
- integrity: cleanOutput(manifest.dist?.integrity, 300) || null,
327
- releaseNotes,
328
- releaseUrl: /^https:\/\//i.test(metadata.releaseUrl || '') ? cleanOutput(metadata.releaseUrl, 500) : null,
329
- };
330
- }
331
-
332
- async check(target) {
333
- const state = await this.readState();
334
- const requestedTarget = normalizeUpdateTarget(target, state.channel);
335
- const [release, installMode] = await Promise.all([
336
- this.fetchRelease(requestedTarget),
337
- this.detectInstallMode(),
338
- ]);
339
- const comparison = compareVersions(release.version, this.currentVersion);
340
- const nodeRange = release.engines?.node || null;
341
- const nodeCompatible = satisfiesNodeEngine(this.nodeVersion, nodeRange);
342
- const current = parseSemver(this.currentVersion);
343
- const next = parseSemver(release.version);
344
- const status = {
345
- packageName: this.packageName,
346
- currentVersion: this.currentVersion,
347
- targetVersion: release.version,
348
- target: requestedTarget,
349
- channel: UPDATE_CHANNELS.includes(requestedTarget) ? requestedTarget : state.channel,
350
- comparison,
351
- change: comparison > 0 ? 'upgrade' : comparison < 0 ? 'downgrade' : 'current',
352
- updateAvailable: comparison > 0,
353
- majorChange: current.major !== next.major,
354
- nodeRange,
355
- nodeCompatible,
356
- installMode,
357
- deprecated: release.deprecated,
358
- integrity: release.integrity,
359
- releaseNotes: release.releaseNotes,
360
- releaseUrl: release.releaseUrl,
361
- };
362
- state.lastCheck = {
363
- checkedAt: new Date().toISOString(),
364
- currentVersion: status.currentVersion,
365
- targetVersion: status.targetVersion,
366
- target: status.target,
367
- };
368
- try { await this.writeState(state); } catch {}
369
- return status;
370
- }
371
-
372
- manualCommand(installMode, version) {
373
- if (installMode.id === 'npx') return `npx --yes --prefer-online ${this.packageName}@${version}`;
374
- if (installMode.id === 'local') return `npm install ${this.packageName}@${version}`;
375
- if (installMode.id === 'source') return 'git pull && npm install';
376
- return `npm install --global ${this.packageName}@${version}`;
377
- }
378
-
379
- async install(target, options = {}) {
380
- const status = await this.check(target);
381
- if (status.deprecated && !options.allowDeprecated) {
382
- throw new UpdateError(`This release is deprecated: ${status.deprecated}`, 'DEPRECATED_RELEASE');
383
- }
384
- if (!status.nodeCompatible) {
385
- throw new UpdateError(
386
- `Node ${this.nodeVersion} does not satisfy ${status.nodeRange}.`,
387
- 'NODE_INCOMPATIBLE'
388
- );
389
- }
390
- if (status.majorChange && status.change === 'upgrade' && !options.allowMajor) {
391
- throw new UpdateError(
392
- `Version ${status.targetVersion} is a major update. Re-run with --allow-major after reviewing its release notes.`,
393
- 'MAJOR_CONFIRMATION_REQUIRED'
394
- );
395
- }
396
- if (status.change === 'current') return { ...status, outcome: 'current' };
397
- if (!status.installMode.automatic) {
398
- return {
399
- ...status,
400
- outcome: 'manual',
401
- command: this.manualCommand(status.installMode, status.targetVersion),
402
- };
403
- }
404
-
405
- const exactSpec = `${this.packageName}@${status.targetVersion}`;
406
- const result = await this.runner(
407
- ['install', '--global', exactSpec, '--no-audit', '--no-fund'],
408
- { timeoutMs: INSTALL_TIMEOUT_MS, onStderr: options.onProgress }
409
- );
410
- if (result.code !== 0) {
411
- throw new UpdateError(
412
- cleanOutput(result.stderr) || 'npm failed to install the update.',
413
- 'INSTALL_FAILED',
414
- { exitCode: result.code, version: status.targetVersion }
415
- );
416
- }
417
-
418
- const verification = await this.runner(
419
- ['list', '--global', this.packageName, '--depth=0', '--json'],
420
- { timeoutMs: DEFAULT_TIMEOUT_MS }
421
- );
422
- if (verification.code !== 0) {
423
- throw new UpdateError('The update installed, but its version could not be verified.', 'VERIFY_FAILED');
424
- }
425
- const installed = safeJsonParse(verification.stdout, 'VERIFY_RESPONSE_INVALID')
426
- ?.dependencies?.[this.packageName]?.version;
427
- if (installed !== status.targetVersion) {
428
- throw new UpdateError('npm reported a different installed version after updating.', 'VERIFY_VERSION_MISMATCH', {
429
- expected: status.targetVersion,
430
- installed,
431
- });
432
- }
433
-
434
- let historyRecorded = true;
435
- try {
436
- const state = await this.readState();
437
- state.installs.push({
438
- from: this.currentVersion,
439
- to: status.targetVersion,
440
- target: status.target,
441
- installedAt: new Date().toISOString(),
442
- });
443
- await this.writeState(state);
444
- } catch {
445
- historyRecorded = false;
446
- }
447
- return { ...status, outcome: 'installed', installedVersion: installed, historyRecorded };
448
- }
449
-
450
- async rollback(options = {}) {
451
- const state = await this.readState();
452
- const last = [...state.installs].reverse().find(item => item.to === this.currentVersion) || state.installs.at(-1);
453
- if (!last) throw new UpdateError('No previous Kiku Agent version is recorded.', 'NO_ROLLBACK_VERSION');
454
- return this.install(last.from, { ...options, allowMajor: true, rollback: true });
455
- }
456
- }