skyloom 1.14.8 → 1.15.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 (156) hide show
  1. package/.github/workflows/ci.yml +2 -2
  2. package/.github/workflows/publish.yml +51 -4
  3. package/CONVERSION_PLAN.md +191 -191
  4. package/config/default.yaml +46 -43
  5. package/config/models.yaml +928 -155
  6. package/config/providers.yaml +109 -6
  7. package/dist/agents/snow.d.ts +2 -0
  8. package/dist/agents/snow.d.ts.map +1 -1
  9. package/dist/agents/snow.js +36 -5
  10. package/dist/agents/snow.js.map +1 -1
  11. package/dist/cli/loom_chat.d.ts.map +1 -1
  12. package/dist/cli/loom_chat.js +207 -1
  13. package/dist/cli/loom_chat.js.map +1 -1
  14. package/dist/cli/main.js +190 -40
  15. package/dist/cli/main.js.map +1 -1
  16. package/dist/cli/tui.d.ts.map +1 -1
  17. package/dist/cli/tui.js +6 -31
  18. package/dist/cli/tui.js.map +1 -1
  19. package/dist/core/agent.d.ts +6 -4
  20. package/dist/core/agent.d.ts.map +1 -1
  21. package/dist/core/agent.js +61 -20
  22. package/dist/core/agent.js.map +1 -1
  23. package/dist/core/catalog.d.ts.map +1 -1
  24. package/dist/core/catalog.js +30 -9
  25. package/dist/core/catalog.js.map +1 -1
  26. package/dist/core/commands.d.ts +110 -0
  27. package/dist/core/commands.d.ts.map +1 -0
  28. package/dist/core/commands.js +633 -0
  29. package/dist/core/commands.js.map +1 -0
  30. package/dist/core/concurrency.d.ts +38 -0
  31. package/dist/core/concurrency.d.ts.map +1 -0
  32. package/dist/core/concurrency.js +65 -0
  33. package/dist/core/concurrency.js.map +1 -0
  34. package/dist/core/factory.js +16 -16
  35. package/dist/core/file_checkpoint.d.ts +9 -0
  36. package/dist/core/file_checkpoint.d.ts.map +1 -1
  37. package/dist/core/file_checkpoint.js +33 -1
  38. package/dist/core/file_checkpoint.js.map +1 -1
  39. package/dist/core/llm.d.ts.map +1 -1
  40. package/dist/core/llm.js +66 -13
  41. package/dist/core/llm.js.map +1 -1
  42. package/dist/core/memory.js +51 -51
  43. package/dist/core/schemas.d.ts +16 -0
  44. package/dist/core/schemas.d.ts.map +1 -1
  45. package/dist/core/schemas.js +32 -0
  46. package/dist/core/schemas.js.map +1 -1
  47. package/dist/core/security.d.ts.map +1 -1
  48. package/dist/core/security.js +27 -0
  49. package/dist/core/security.js.map +1 -1
  50. package/dist/core/skymd.js +14 -14
  51. package/dist/core/trace.d.ts +105 -0
  52. package/dist/core/trace.d.ts.map +1 -0
  53. package/dist/core/trace.js +213 -0
  54. package/dist/core/trace.js.map +1 -0
  55. package/dist/tools/builtin.d.ts +2 -6
  56. package/dist/tools/builtin.d.ts.map +1 -1
  57. package/dist/tools/builtin.js +18 -111
  58. package/dist/tools/builtin.js.map +1 -1
  59. package/dist/tools/extra.d.ts +13 -0
  60. package/dist/tools/extra.d.ts.map +1 -0
  61. package/dist/tools/extra.js +827 -0
  62. package/dist/tools/extra.js.map +1 -0
  63. package/dist/tools/guards.d.ts +12 -0
  64. package/dist/tools/guards.d.ts.map +1 -0
  65. package/dist/tools/guards.js +143 -0
  66. package/dist/tools/guards.js.map +1 -0
  67. package/dist/tools/model_tool.d.ts.map +1 -1
  68. package/dist/tools/model_tool.js +24 -4
  69. package/dist/tools/model_tool.js.map +1 -1
  70. package/dist/web/markdown.d.ts +32 -0
  71. package/dist/web/markdown.d.ts.map +1 -0
  72. package/dist/web/markdown.js +202 -0
  73. package/dist/web/markdown.js.map +1 -0
  74. package/dist/web/server.d.ts +4 -0
  75. package/dist/web/server.d.ts.map +1 -1
  76. package/dist/web/server.js +14 -582
  77. package/dist/web/server.js.map +1 -1
  78. package/dist/web/ui.d.ts +31 -0
  79. package/dist/web/ui.d.ts.map +1 -0
  80. package/dist/web/ui.js +1009 -0
  81. package/dist/web/ui.js.map +1 -0
  82. package/docs/AESTHETIC_DESIGN.md +152 -152
  83. package/docs/OPTIMIZATION_PLAN.md +178 -178
  84. package/package.json +1 -1
  85. package/src/agents/snow.ts +38 -5
  86. package/src/cli/commands_md.ts +112 -112
  87. package/src/cli/input_macros.ts +83 -83
  88. package/src/cli/loom.ts +1041 -1041
  89. package/src/cli/loom_chat.ts +772 -603
  90. package/src/cli/main.ts +853 -723
  91. package/src/cli/tui.ts +264 -289
  92. package/src/core/agent/guard.ts +133 -133
  93. package/src/core/agent/task.ts +100 -100
  94. package/src/core/agent.ts +1630 -1590
  95. package/src/core/agent_helpers.ts +500 -500
  96. package/src/core/bus.ts +221 -221
  97. package/src/core/cache.ts +153 -153
  98. package/src/core/catalog.ts +199 -178
  99. package/src/core/circuit_breaker.ts +119 -119
  100. package/src/core/commands.ts +704 -0
  101. package/src/core/concurrency.ts +73 -0
  102. package/src/core/config.ts +365 -365
  103. package/src/core/constants.ts +95 -95
  104. package/src/core/factory.ts +656 -656
  105. package/src/core/file_checkpoint.ts +163 -136
  106. package/src/core/hooks.ts +126 -126
  107. package/src/core/llm.ts +972 -915
  108. package/src/core/logger.ts +143 -143
  109. package/src/core/mcp.ts +1001 -1001
  110. package/src/core/memory.ts +1201 -1201
  111. package/src/core/middleware.ts +350 -350
  112. package/src/core/model_config.ts +159 -159
  113. package/src/core/pipelines.ts +424 -424
  114. package/src/core/schemas.ts +319 -282
  115. package/src/core/security.ts +27 -0
  116. package/src/core/semantic.ts +211 -211
  117. package/src/core/skill.ts +384 -384
  118. package/src/core/skymd.ts +143 -143
  119. package/src/core/theme.ts +65 -65
  120. package/src/core/tool.ts +457 -457
  121. package/src/core/trace.ts +236 -0
  122. package/src/core/verify.ts +71 -71
  123. package/src/plugins/loader.ts +91 -91
  124. package/src/skills/loader.ts +75 -75
  125. package/src/tools/builtin.ts +571 -642
  126. package/src/tools/computer.ts +279 -279
  127. package/src/tools/extra.ts +662 -0
  128. package/src/tools/guards.ts +82 -0
  129. package/src/tools/model_tool.ts +93 -74
  130. package/src/tools/todo.ts +76 -76
  131. package/src/web/markdown.ts +193 -0
  132. package/src/web/server.ts +117 -693
  133. package/src/web/ui.ts +949 -0
  134. package/tests/agent.test.ts +211 -159
  135. package/tests/agent_helpers.test.ts +48 -48
  136. package/tests/catalog.test.ts +86 -86
  137. package/tests/checkpoint_commands.test.ts +124 -124
  138. package/tests/claude_compat.test.ts +110 -110
  139. package/tests/commands.test.ts +103 -0
  140. package/tests/concurrency.test.ts +102 -0
  141. package/tests/config.test.ts +41 -41
  142. package/tests/extra_tools.test.ts +212 -0
  143. package/tests/fence_plugin.test.ts +52 -52
  144. package/tests/guard.test.ts +75 -75
  145. package/tests/loom.test.ts +337 -337
  146. package/tests/memory.test.ts +170 -170
  147. package/tests/model_config.test.ts +109 -109
  148. package/tests/skymd.test.ts +146 -146
  149. package/tests/ssrf.test.ts +38 -38
  150. package/tests/structured_retry.test.ts +87 -0
  151. package/tests/task.test.ts +60 -60
  152. package/tests/todo_toolstats.test.ts +94 -94
  153. package/tests/trace.test.ts +128 -0
  154. package/tests/tui.test.ts +67 -67
  155. package/tests/web.test.ts +169 -0
  156. package/tsconfig.json +38 -38
package/src/core/skymd.ts CHANGED
@@ -1,143 +1,143 @@
1
- /**
2
- * 项目记忆 (SKY.md) — Skyloom's equivalent of Claude Code's CLAUDE.md.
3
- *
4
- * A layered, auto-loaded instruction file that gives every agent durable
5
- * knowledge of the project: build commands, conventions, constraints.
6
- *
7
- * Load order (all layers concatenate, later = more specific):
8
- * 1. ~/.skyloom/SKY.md — user level, applies to every project
9
- * 2. ./SKY.md | ./CLAUDE.md | ./AGENTS.md — project level (first match;
10
- * CLAUDE.md/AGENTS.md compatibility means existing repos work as-is)
11
- * 3. ./SKY.local.md — project level, personal (gitignored)
12
- *
13
- * The merged text is injected into every agent's system prompt at init.
14
- * `#<note>` in chat appends to the project file via appendQuickMemory().
15
- */
16
-
17
- import * as fs from 'fs';
18
- import * as os from 'os';
19
- import * as path from 'path';
20
-
21
- /** Total budget for injected memory — it lives in every prompt, keep it lean. */
22
- const MAX_MEMORY_CHARS = 12000;
23
- /** Per-file budget so one bloated layer can't crowd out the others. */
24
- const MAX_FILE_CHARS = 6000;
25
-
26
- export interface ProjectMemory {
27
- /** Merged, truncated text ready for prompt injection ('' if no files). */
28
- text: string;
29
- /** Absolute paths of the files that contributed. */
30
- files: string[];
31
- }
32
-
33
- const PROJECT_FILE_CANDIDATES = ['SKY.md', 'CLAUDE.md', 'AGENTS.md'];
34
-
35
- function readClamped(p: string): string | null {
36
- try {
37
- if (!fs.existsSync(p)) return null;
38
- const raw = fs.readFileSync(p, 'utf-8').trim();
39
- if (!raw) return null;
40
- return raw.length > MAX_FILE_CHARS
41
- ? raw.slice(0, MAX_FILE_CHARS) + '\n…[truncated]'
42
- : raw;
43
- } catch {
44
- return null;
45
- }
46
- }
47
-
48
- /** Resolve the project-level memory file (existing first candidate, or null). */
49
- export function projectMemoryFile(cwd: string = process.cwd()): string | null {
50
- for (const name of PROJECT_FILE_CANDIDATES) {
51
- const p = path.join(cwd, name);
52
- if (fs.existsSync(p)) return p;
53
- }
54
- return null;
55
- }
56
-
57
- /** Load and merge all memory layers for prompt injection. */
58
- export function loadProjectMemory(cwd: string = process.cwd()): ProjectMemory {
59
- const layers: Array<{ label: string; file: string }> = [];
60
-
61
- const userFile = path.join(os.homedir(), '.skyloom', 'SKY.md');
62
- layers.push({ label: '用户级', file: userFile });
63
-
64
- const projFile = projectMemoryFile(cwd);
65
- if (projFile) layers.push({ label: '项目级', file: projFile });
66
-
67
- const localFile = path.join(cwd, 'SKY.local.md');
68
- layers.push({ label: '本地', file: localFile });
69
-
70
- const parts: string[] = [];
71
- const files: string[] = [];
72
- for (const { label, file } of layers) {
73
- const content = readClamped(file);
74
- if (content === null) continue;
75
- parts.push(`<!-- ${label}: ${path.basename(file)} -->\n${content}`);
76
- files.push(file);
77
- }
78
-
79
- let text = parts.join('\n\n');
80
- if (text.length > MAX_MEMORY_CHARS) text = text.slice(0, MAX_MEMORY_CHARS) + '\n…[truncated]';
81
- return { text, files };
82
- }
83
-
84
- const SKY_MD_HEADER = `# SKY.md — 项目记忆
85
-
86
- > Skyloom agents 启动时自动加载本文件。写团队约定、构建/测试命令、代码风格。
87
- > 对话中输入 \`#内容\` 可快速追加一条。
88
-
89
- `;
90
-
91
- /**
92
- * Append a one-line note (the \`#\` quick-memory flow).
93
- * Writes to the existing project memory file, or creates ./SKY.md.
94
- * Returns the file path written.
95
- */
96
- export function appendQuickMemory(note: string, cwd: string = process.cwd()): string {
97
- const target = projectMemoryFile(cwd) ?? path.join(cwd, 'SKY.md');
98
- const line = `- ${note.trim()}\n`;
99
- if (!fs.existsSync(target)) {
100
- fs.writeFileSync(target, SKY_MD_HEADER + line, 'utf-8');
101
- } else {
102
- const existing = fs.readFileSync(target, 'utf-8');
103
- fs.appendFileSync(target, (existing.endsWith('\n') ? '' : '\n') + line, 'utf-8');
104
- }
105
- return target;
106
- }
107
-
108
- /**
109
- * Extract verify commands from a "## Verify" / "## 验证" section of the
110
- * merged memory text: each non-comment line of its fenced code block.
111
- */
112
- export function parseVerifyCommands(memoryText: string): string[] {
113
- const cmds: string[] = [];
114
- let inSection = false;
115
- let inFence = false;
116
- for (const line of memoryText.split('\n')) {
117
- if (/^##\s*(verify|验证)/i.test(line)) { inSection = true; continue; }
118
- if (!inSection) continue;
119
- if (/^##\s/.test(line)) break; // next section
120
- if (line.trim().startsWith('```')) {
121
- if (inFence) break; // closing fence — done
122
- inFence = true;
123
- continue;
124
- }
125
- if (inFence) {
126
- const t = line.trim();
127
- if (t && !t.startsWith('#') && !t.startsWith('//')) cmds.push(t);
128
- }
129
- }
130
- return cmds;
131
- }
132
-
133
- /** The prompt behind /init — asks the agent to study the repo and write SKY.md. */
134
- export const INIT_PROMPT = `请为当前项目生成一份 SKY.md 项目记忆文件(如已存在则改进它):
135
-
136
- 1. 用工具调研项目:读 README、package.json/pyproject.toml/Cargo.toml 等清单文件,list_directory 看结构,必要时读关键入口源码
137
- 2. 写出一份**短而精**的 SKY.md(每一行都占用所有 agent 的上下文,宁缺毋滥),包含:
138
- - 项目一句话定位与技术栈
139
- - 构建/测试/lint 命令(放在 "## Verify" 小节的代码块里,agents 会用它自动验证)
140
- - 目录结构要点(只列关键目录)
141
- - 代码风格与硬性约束(如「禁止 any」「提交信息格式」)
142
- 3. 用 write_file 写到项目根目录 SKY.md
143
- 4. 最后简要汇报你写了什么`;
1
+ /**
2
+ * 项目记忆 (SKY.md) — Skyloom's equivalent of Claude Code's CLAUDE.md.
3
+ *
4
+ * A layered, auto-loaded instruction file that gives every agent durable
5
+ * knowledge of the project: build commands, conventions, constraints.
6
+ *
7
+ * Load order (all layers concatenate, later = more specific):
8
+ * 1. ~/.skyloom/SKY.md — user level, applies to every project
9
+ * 2. ./SKY.md | ./CLAUDE.md | ./AGENTS.md — project level (first match;
10
+ * CLAUDE.md/AGENTS.md compatibility means existing repos work as-is)
11
+ * 3. ./SKY.local.md — project level, personal (gitignored)
12
+ *
13
+ * The merged text is injected into every agent's system prompt at init.
14
+ * `#<note>` in chat appends to the project file via appendQuickMemory().
15
+ */
16
+
17
+ import * as fs from 'fs';
18
+ import * as os from 'os';
19
+ import * as path from 'path';
20
+
21
+ /** Total budget for injected memory — it lives in every prompt, keep it lean. */
22
+ const MAX_MEMORY_CHARS = 12000;
23
+ /** Per-file budget so one bloated layer can't crowd out the others. */
24
+ const MAX_FILE_CHARS = 6000;
25
+
26
+ export interface ProjectMemory {
27
+ /** Merged, truncated text ready for prompt injection ('' if no files). */
28
+ text: string;
29
+ /** Absolute paths of the files that contributed. */
30
+ files: string[];
31
+ }
32
+
33
+ const PROJECT_FILE_CANDIDATES = ['SKY.md', 'CLAUDE.md', 'AGENTS.md'];
34
+
35
+ function readClamped(p: string): string | null {
36
+ try {
37
+ if (!fs.existsSync(p)) return null;
38
+ const raw = fs.readFileSync(p, 'utf-8').trim();
39
+ if (!raw) return null;
40
+ return raw.length > MAX_FILE_CHARS
41
+ ? raw.slice(0, MAX_FILE_CHARS) + '\n…[truncated]'
42
+ : raw;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /** Resolve the project-level memory file (existing first candidate, or null). */
49
+ export function projectMemoryFile(cwd: string = process.cwd()): string | null {
50
+ for (const name of PROJECT_FILE_CANDIDATES) {
51
+ const p = path.join(cwd, name);
52
+ if (fs.existsSync(p)) return p;
53
+ }
54
+ return null;
55
+ }
56
+
57
+ /** Load and merge all memory layers for prompt injection. */
58
+ export function loadProjectMemory(cwd: string = process.cwd()): ProjectMemory {
59
+ const layers: Array<{ label: string; file: string }> = [];
60
+
61
+ const userFile = path.join(os.homedir(), '.skyloom', 'SKY.md');
62
+ layers.push({ label: '用户级', file: userFile });
63
+
64
+ const projFile = projectMemoryFile(cwd);
65
+ if (projFile) layers.push({ label: '项目级', file: projFile });
66
+
67
+ const localFile = path.join(cwd, 'SKY.local.md');
68
+ layers.push({ label: '本地', file: localFile });
69
+
70
+ const parts: string[] = [];
71
+ const files: string[] = [];
72
+ for (const { label, file } of layers) {
73
+ const content = readClamped(file);
74
+ if (content === null) continue;
75
+ parts.push(`<!-- ${label}: ${path.basename(file)} -->\n${content}`);
76
+ files.push(file);
77
+ }
78
+
79
+ let text = parts.join('\n\n');
80
+ if (text.length > MAX_MEMORY_CHARS) text = text.slice(0, MAX_MEMORY_CHARS) + '\n…[truncated]';
81
+ return { text, files };
82
+ }
83
+
84
+ const SKY_MD_HEADER = `# SKY.md — 项目记忆
85
+
86
+ > Skyloom agents 启动时自动加载本文件。写团队约定、构建/测试命令、代码风格。
87
+ > 对话中输入 \`#内容\` 可快速追加一条。
88
+
89
+ `;
90
+
91
+ /**
92
+ * Append a one-line note (the \`#\` quick-memory flow).
93
+ * Writes to the existing project memory file, or creates ./SKY.md.
94
+ * Returns the file path written.
95
+ */
96
+ export function appendQuickMemory(note: string, cwd: string = process.cwd()): string {
97
+ const target = projectMemoryFile(cwd) ?? path.join(cwd, 'SKY.md');
98
+ const line = `- ${note.trim()}\n`;
99
+ if (!fs.existsSync(target)) {
100
+ fs.writeFileSync(target, SKY_MD_HEADER + line, 'utf-8');
101
+ } else {
102
+ const existing = fs.readFileSync(target, 'utf-8');
103
+ fs.appendFileSync(target, (existing.endsWith('\n') ? '' : '\n') + line, 'utf-8');
104
+ }
105
+ return target;
106
+ }
107
+
108
+ /**
109
+ * Extract verify commands from a "## Verify" / "## 验证" section of the
110
+ * merged memory text: each non-comment line of its fenced code block.
111
+ */
112
+ export function parseVerifyCommands(memoryText: string): string[] {
113
+ const cmds: string[] = [];
114
+ let inSection = false;
115
+ let inFence = false;
116
+ for (const line of memoryText.split('\n')) {
117
+ if (/^##\s*(verify|验证)/i.test(line)) { inSection = true; continue; }
118
+ if (!inSection) continue;
119
+ if (/^##\s/.test(line)) break; // next section
120
+ if (line.trim().startsWith('```')) {
121
+ if (inFence) break; // closing fence — done
122
+ inFence = true;
123
+ continue;
124
+ }
125
+ if (inFence) {
126
+ const t = line.trim();
127
+ if (t && !t.startsWith('#') && !t.startsWith('//')) cmds.push(t);
128
+ }
129
+ }
130
+ return cmds;
131
+ }
132
+
133
+ /** The prompt behind /init — asks the agent to study the repo and write SKY.md. */
134
+ export const INIT_PROMPT = `请为当前项目生成一份 SKY.md 项目记忆文件(如已存在则改进它):
135
+
136
+ 1. 用工具调研项目:读 README、package.json/pyproject.toml/Cargo.toml 等清单文件,list_directory 看结构,必要时读关键入口源码
137
+ 2. 写出一份**短而精**的 SKY.md(每一行都占用所有 agent 的上下文,宁缺毋滥),包含:
138
+ - 项目一句话定位与技术栈
139
+ - 构建/测试/lint 命令(放在 "## Verify" 小节的代码块里,agents 会用它自动验证)
140
+ - 目录结构要点(只列关键目录)
141
+ - 代码风格与硬性约束(如「禁止 any」「提交信息格式」)
142
+ 3. 用 write_file 写到项目根目录 SKY.md
143
+ 4. 最后简要汇报你写了什么`;
package/src/core/theme.ts CHANGED
@@ -1,65 +1,65 @@
1
- /**
2
- * Design tokens — single source of truth for Skyloom's "水墨气象台" identity.
3
- *
4
- * One definition drives every surface: CLI (chalk truecolor), the full-screen
5
- * TUI, and the Web ink-wash UI. Change a pigment here and all three follow.
6
- *
7
- * Design rationale: docs/AESTHETIC_DESIGN.md
8
- */
9
-
10
- /** Base paper-and-ink palette (hex). */
11
- export const PALETTE = {
12
- paper: "#f8f4ec",
13
- paperWarm: "#f3ede2",
14
- inkDeep: "#1a1614",
15
- inkMid: "#3d3833",
16
- inkLight: "#8c8680",
17
- inkFaint: "#c4bfb8",
18
- } as const;
19
-
20
- /** Per-agent identity: weather + mineral pigment + classical poem + motion. */
21
- export interface AgentTheme {
22
- /** Agent key (fog/rain/…). */
23
- name: string;
24
- /** Weather kanji used as a seal stamp (霧/雨/…). */
25
- kanji: string;
26
- /** Single-glyph weather symbol used across CLI/TUI/Web. */
27
- symbol: string;
28
- /** Mineral pigment hex. */
29
- hex: string;
30
- /** Mineral pigment RGB triple (for ANSI truecolor / CSS rgba). */
31
- rgb: [number, number, number];
32
- /** Pigment name in Chinese (松烟墨/石青/…). */
33
- pigment: string;
34
- /** Responsibility (探索洞察/创造产出/…). */
35
- specialty: string;
36
- /** Classical poem line shown in empty states / sidebars. */
37
- poem: string;
38
- /** Motion keyword (drift/fall/glint/float/bead/rise). */
39
- motion: string;
40
- }
41
-
42
- function rgbOf(hex: string): [number, number, number] {
43
- const h = hex.replace("#", "");
44
- return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
45
- }
46
-
47
- export const AGENT_THEMES: Record<string, AgentTheme> = {
48
- fog: { name: "fog", kanji: "霧", symbol: "≋", hex: "#4a4a44", rgb: rgbOf("#4a4a44"), pigment: "松烟墨", specialty: "探索洞察", poem: "山色有无中", motion: "drift" },
49
- rain: { name: "rain", kanji: "雨", symbol: "⸽", hex: "#2a5c8a", rgb: rgbOf("#2a5c8a"), pigment: "石青", specialty: "创造产出", poem: "一蓑烟雨任平生", motion: "fall" },
50
- frost: { name: "frost", kanji: "霜", symbol: "✱", hex: "#3a7a6e", rgb: rgbOf("#3a7a6e"), pigment: "石绿", specialty: "精炼品质", poem: "月落乌啼霜满天", motion: "glint" },
51
- snow: { name: "snow", kanji: "雪", symbol: "❉", hex: "#8a8a82", rgb: rgbOf("#8a8a82"), pigment: "铅白", specialty: "架构规划", poem: "千树万树梨花开", motion: "float" },
52
- dew: { name: "dew", kanji: "露", symbol: "∘", hex: "#8b6914", rgb: rgbOf("#8b6914"), pigment: "赭石", specialty: "可靠守护", poem: "金风玉露一相逢", motion: "bead" },
53
- fair: { name: "fair", kanji: "晴", symbol: "☼", hex: "#b3342d", rgb: rgbOf("#b3342d"), pigment: "朱砂", specialty: "情感陪伴", poem: "道是无晴却有晴", motion: "rise" },
54
- };
55
-
56
- /** Ordered agent keys (織機 six shuttles). */
57
- export const AGENT_ORDER = ["fog", "rain", "frost", "snow", "dew", "fair"] as const;
58
-
59
- /** Brand seal pigment (朱砂) — used for the active-agent stamp everywhere. */
60
- export const SEAL_HEX = AGENT_THEMES.fair.hex;
61
-
62
- /** Look up an agent theme, defaulting to fog. */
63
- export function agentTheme(name: string): AgentTheme {
64
- return AGENT_THEMES[name] ?? AGENT_THEMES.fog;
65
- }
1
+ /**
2
+ * Design tokens — single source of truth for Skyloom's "水墨气象台" identity.
3
+ *
4
+ * One definition drives every surface: CLI (chalk truecolor), the full-screen
5
+ * TUI, and the Web ink-wash UI. Change a pigment here and all three follow.
6
+ *
7
+ * Design rationale: docs/AESTHETIC_DESIGN.md
8
+ */
9
+
10
+ /** Base paper-and-ink palette (hex). */
11
+ export const PALETTE = {
12
+ paper: "#f8f4ec",
13
+ paperWarm: "#f3ede2",
14
+ inkDeep: "#1a1614",
15
+ inkMid: "#3d3833",
16
+ inkLight: "#8c8680",
17
+ inkFaint: "#c4bfb8",
18
+ } as const;
19
+
20
+ /** Per-agent identity: weather + mineral pigment + classical poem + motion. */
21
+ export interface AgentTheme {
22
+ /** Agent key (fog/rain/…). */
23
+ name: string;
24
+ /** Weather kanji used as a seal stamp (霧/雨/…). */
25
+ kanji: string;
26
+ /** Single-glyph weather symbol used across CLI/TUI/Web. */
27
+ symbol: string;
28
+ /** Mineral pigment hex. */
29
+ hex: string;
30
+ /** Mineral pigment RGB triple (for ANSI truecolor / CSS rgba). */
31
+ rgb: [number, number, number];
32
+ /** Pigment name in Chinese (松烟墨/石青/…). */
33
+ pigment: string;
34
+ /** Responsibility (探索洞察/创造产出/…). */
35
+ specialty: string;
36
+ /** Classical poem line shown in empty states / sidebars. */
37
+ poem: string;
38
+ /** Motion keyword (drift/fall/glint/float/bead/rise). */
39
+ motion: string;
40
+ }
41
+
42
+ function rgbOf(hex: string): [number, number, number] {
43
+ const h = hex.replace("#", "");
44
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
45
+ }
46
+
47
+ export const AGENT_THEMES: Record<string, AgentTheme> = {
48
+ fog: { name: "fog", kanji: "霧", symbol: "≋", hex: "#4a4a44", rgb: rgbOf("#4a4a44"), pigment: "松烟墨", specialty: "探索洞察", poem: "山色有无中", motion: "drift" },
49
+ rain: { name: "rain", kanji: "雨", symbol: "⸽", hex: "#2a5c8a", rgb: rgbOf("#2a5c8a"), pigment: "石青", specialty: "创造产出", poem: "一蓑烟雨任平生", motion: "fall" },
50
+ frost: { name: "frost", kanji: "霜", symbol: "✱", hex: "#3a7a6e", rgb: rgbOf("#3a7a6e"), pigment: "石绿", specialty: "精炼品质", poem: "月落乌啼霜满天", motion: "glint" },
51
+ snow: { name: "snow", kanji: "雪", symbol: "❉", hex: "#8a8a82", rgb: rgbOf("#8a8a82"), pigment: "铅白", specialty: "架构规划", poem: "千树万树梨花开", motion: "float" },
52
+ dew: { name: "dew", kanji: "露", symbol: "∘", hex: "#8b6914", rgb: rgbOf("#8b6914"), pigment: "赭石", specialty: "可靠守护", poem: "金风玉露一相逢", motion: "bead" },
53
+ fair: { name: "fair", kanji: "晴", symbol: "☼", hex: "#b3342d", rgb: rgbOf("#b3342d"), pigment: "朱砂", specialty: "情感陪伴", poem: "道是无晴却有晴", motion: "rise" },
54
+ };
55
+
56
+ /** Ordered agent keys (織機 six shuttles). */
57
+ export const AGENT_ORDER = ["fog", "rain", "frost", "snow", "dew", "fair"] as const;
58
+
59
+ /** Brand seal pigment (朱砂) — used for the active-agent stamp everywhere. */
60
+ export const SEAL_HEX = AGENT_THEMES.fair.hex;
61
+
62
+ /** Look up an agent theme, defaulting to fog. */
63
+ export function agentTheme(name: string): AgentTheme {
64
+ return AGENT_THEMES[name] ?? AGENT_THEMES.fog;
65
+ }