skyloom 1.12.0 → 1.13.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 (135) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +142 -46
  3. package/config/default.yaml +43 -47
  4. package/config/models.yaml +155 -155
  5. package/config/providers.yaml +39 -39
  6. package/config/skills/api_integrator/SKILL.md +15 -15
  7. package/config/skills/arch_designer/SKILL.md +13 -13
  8. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  9. package/config/skills/code_analysis/SKILL.md +13 -13
  10. package/config/skills/code_generator/SKILL.md +12 -12
  11. package/config/skills/code_reviewer/SKILL.md +13 -13
  12. package/config/skills/content_writer/SKILL.md +14 -14
  13. package/config/skills/data_transformer/SKILL.md +15 -15
  14. package/config/skills/document_analysis/SKILL.md +13 -13
  15. package/config/skills/emotional_companion/SKILL.md +15 -15
  16. package/config/skills/performance_checker/SKILL.md +14 -14
  17. package/config/skills/security_auditor/SKILL.md +14 -14
  18. package/config/skills/self_evolve/SKILL.md +13 -13
  19. package/config/skills/sys_operator/SKILL.md +15 -15
  20. package/config/skills/task_planner/SKILL.md +14 -14
  21. package/config/skills/web_research/SKILL.md +14 -14
  22. package/config/skills/workflow_designer/SKILL.md +13 -13
  23. package/dist/agents/dew.js +52 -52
  24. package/dist/agents/fair.js +84 -84
  25. package/dist/agents/fog.js +30 -30
  26. package/dist/agents/frost.js +32 -32
  27. package/dist/agents/rain.js +32 -32
  28. package/dist/agents/snow.js +68 -68
  29. package/dist/cli/main.js +103 -51
  30. package/dist/cli/main.js.map +1 -1
  31. package/dist/cli/tui.d.ts.map +1 -1
  32. package/dist/cli/tui.js +8 -1
  33. package/dist/cli/tui.js.map +1 -1
  34. package/dist/core/agent/task.d.ts +58 -0
  35. package/dist/core/agent/task.d.ts.map +1 -0
  36. package/dist/core/agent/task.js +83 -0
  37. package/dist/core/agent/task.js.map +1 -0
  38. package/dist/core/agent.d.ts +2 -45
  39. package/dist/core/agent.d.ts.map +1 -1
  40. package/dist/core/agent.js +61 -145
  41. package/dist/core/agent.js.map +1 -1
  42. package/dist/core/agent_helpers.d.ts +10 -0
  43. package/dist/core/agent_helpers.d.ts.map +1 -1
  44. package/dist/core/agent_helpers.js +39 -0
  45. package/dist/core/agent_helpers.js.map +1 -1
  46. package/dist/core/catalog.d.ts +71 -0
  47. package/dist/core/catalog.d.ts.map +1 -0
  48. package/dist/core/catalog.js +176 -0
  49. package/dist/core/catalog.js.map +1 -0
  50. package/dist/core/config.d.ts +8 -0
  51. package/dist/core/config.d.ts.map +1 -1
  52. package/dist/core/config.js +12 -4
  53. package/dist/core/config.js.map +1 -1
  54. package/dist/core/factory.js +16 -16
  55. package/dist/core/llm.d.ts +7 -0
  56. package/dist/core/llm.d.ts.map +1 -1
  57. package/dist/core/llm.js +139 -7
  58. package/dist/core/llm.js.map +1 -1
  59. package/dist/core/longdoc.js +5 -5
  60. package/dist/core/memory.d.ts.map +1 -1
  61. package/dist/core/memory.js +69 -62
  62. package/dist/core/memory.js.map +1 -1
  63. package/dist/core/theme.d.ts +46 -0
  64. package/dist/core/theme.d.ts.map +1 -0
  65. package/dist/core/theme.js +42 -0
  66. package/dist/core/theme.js.map +1 -0
  67. package/dist/web/server.js +542 -519
  68. package/dist/web/server.js.map +1 -1
  69. package/docs/AESTHETIC_DESIGN.md +144 -0
  70. package/docs/OPTIMIZATION_PLAN.md +178 -0
  71. package/package.json +60 -60
  72. package/scripts/install.js +48 -48
  73. package/scripts/link.js +10 -10
  74. package/setup.bat +79 -79
  75. package/skill-test-ty2fOA/test.md +10 -10
  76. package/src/agents/dew.ts +70 -70
  77. package/src/agents/fair.ts +102 -102
  78. package/src/agents/fog.ts +48 -48
  79. package/src/agents/frost.ts +50 -50
  80. package/src/agents/rain.ts +50 -50
  81. package/src/agents/snow.ts +239 -239
  82. package/src/cli/main.ts +425 -372
  83. package/src/cli/mode.ts +58 -58
  84. package/src/cli/tui.ts +272 -269
  85. package/src/core/agent/task.ts +100 -0
  86. package/src/core/agent.ts +1446 -1549
  87. package/src/core/agent_helpers.ts +496 -461
  88. package/src/core/arbitrate.ts +162 -162
  89. package/src/core/catalog.ts +178 -0
  90. package/src/core/checkpoint.ts +94 -94
  91. package/src/core/config.ts +20 -4
  92. package/src/core/estimate.ts +104 -104
  93. package/src/core/evolve.ts +191 -191
  94. package/src/core/factory.ts +627 -627
  95. package/src/core/filter.ts +103 -103
  96. package/src/core/graph.ts +156 -156
  97. package/src/core/icons.ts +53 -53
  98. package/src/core/index.ts +37 -37
  99. package/src/core/learn.ts +146 -146
  100. package/src/core/llm.ts +108 -5
  101. package/src/core/longdoc.ts +155 -155
  102. package/src/core/mcp_server.ts +176 -176
  103. package/src/core/memory.ts +1178 -1171
  104. package/src/core/profile.ts +255 -255
  105. package/src/core/router.ts +124 -124
  106. package/src/core/sandbox.ts +142 -142
  107. package/src/core/security.ts +243 -243
  108. package/src/core/skill.ts +342 -342
  109. package/src/core/theme.ts +65 -0
  110. package/src/core/tool_router.ts +193 -193
  111. package/src/core/vector.ts +152 -152
  112. package/src/core/workspace.ts +150 -150
  113. package/src/plugins/loader.ts +66 -66
  114. package/src/skills/loader.ts +46 -46
  115. package/src/sql.js.d.ts +29 -29
  116. package/src/tools/builtin.ts +380 -380
  117. package/src/tools/computer.ts +269 -269
  118. package/src/tools/delegate.ts +49 -49
  119. package/src/web/server.ts +660 -634
  120. package/src/web/tts.ts +93 -93
  121. package/tests/agent_helpers.test.ts +48 -0
  122. package/tests/bus.test.ts +121 -121
  123. package/tests/catalog.test.ts +86 -0
  124. package/tests/config.test.ts +41 -0
  125. package/tests/icons.test.ts +45 -45
  126. package/tests/memory.test.ts +147 -0
  127. package/tests/router.test.ts +86 -86
  128. package/tests/schemas.test.ts +51 -51
  129. package/tests/semantic.test.ts +83 -83
  130. package/tests/setup.ts +10 -10
  131. package/tests/skill.test.ts +172 -172
  132. package/tests/task.test.ts +60 -0
  133. package/tests/tool.test.ts +108 -108
  134. package/tests/tool_router.test.ts +71 -71
  135. package/vitest.config.ts +17 -17
@@ -1,94 +1,94 @@
1
- /**
2
- * Orchestration checkpoint — save/restore task state.
3
- *
4
- * Writes ~/.skyloom/task_checkpoint.json so a long-running orchestration
5
- * interrupted by Ctrl-C can be resumed.
6
- */
7
-
8
- import * as fs from 'fs';
9
- import * as path from 'path';
10
- import { USER_CONFIG_DIR } from './config';
11
-
12
- function checkpointPath(): string {
13
- return path.join(USER_CONFIG_DIR, 'task_checkpoint.json');
14
- }
15
-
16
- /**
17
- * Save current orchestration state so it can be resumed later.
18
- */
19
- export function save(
20
- goal: string,
21
- tasks: any[],
22
- results: any[],
23
- completedIds?: Set<string>
24
- ): void {
25
- const cids = completedIds || new Set(results.map((r: any) => r.id));
26
- const payload = {
27
- goal,
28
- tasks: tasks.map(serializeTask),
29
- results: results.map(serializeResult),
30
- completed_ids: Array.from(cids).sort(),
31
- };
32
-
33
- const p = checkpointPath();
34
- const dir = path.dirname(p);
35
- if (!fs.existsSync(dir)) {
36
- fs.mkdirSync(dir, { recursive: true });
37
- }
38
-
39
- const tmp = p + '.tmp';
40
- fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
41
- fs.renameSync(tmp, p);
42
- }
43
-
44
- /**
45
- * Return the last saved checkpoint dict, or null if none / unreadable.
46
- */
47
- export function load(): Record<string, any> | null {
48
- const p = checkpointPath();
49
- if (!fs.existsSync(p)) {
50
- return null;
51
- }
52
- try {
53
- const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
54
- return typeof data === 'object' && data !== null ? data : null;
55
- } catch {
56
- return null;
57
- }
58
- }
59
-
60
- /**
61
- * Delete the checkpoint file.
62
- */
63
- export function clear(): void {
64
- try {
65
- const p = checkpointPath();
66
- if (fs.existsSync(p)) {
67
- fs.unlinkSync(p);
68
- }
69
- } catch {
70
- // Ignore cleanup errors
71
- }
72
- }
73
-
74
- // ── Serialization helpers ──
75
-
76
- function serializeTask(t: any): Record<string, any> {
77
- return {
78
- id: t.id,
79
- description: t.description,
80
- assigned_to: t.assignedTo ?? t.assigned_to,
81
- all_deps: t.allDeps ?? t.all_deps ?? [],
82
- status: t.status?.value ?? t.status ?? 'unknown',
83
- };
84
- }
85
-
86
- function serializeResult(r: any): Record<string, any> {
87
- return {
88
- id: r.id,
89
- agent: r.agent,
90
- description: r.description,
91
- success: r.success,
92
- content: r.content,
93
- };
94
- }
1
+ /**
2
+ * Orchestration checkpoint — save/restore task state.
3
+ *
4
+ * Writes ~/.skyloom/task_checkpoint.json so a long-running orchestration
5
+ * interrupted by Ctrl-C can be resumed.
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { USER_CONFIG_DIR } from './config';
11
+
12
+ function checkpointPath(): string {
13
+ return path.join(USER_CONFIG_DIR, 'task_checkpoint.json');
14
+ }
15
+
16
+ /**
17
+ * Save current orchestration state so it can be resumed later.
18
+ */
19
+ export function save(
20
+ goal: string,
21
+ tasks: any[],
22
+ results: any[],
23
+ completedIds?: Set<string>
24
+ ): void {
25
+ const cids = completedIds || new Set(results.map((r: any) => r.id));
26
+ const payload = {
27
+ goal,
28
+ tasks: tasks.map(serializeTask),
29
+ results: results.map(serializeResult),
30
+ completed_ids: Array.from(cids).sort(),
31
+ };
32
+
33
+ const p = checkpointPath();
34
+ const dir = path.dirname(p);
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+
39
+ const tmp = p + '.tmp';
40
+ fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
41
+ fs.renameSync(tmp, p);
42
+ }
43
+
44
+ /**
45
+ * Return the last saved checkpoint dict, or null if none / unreadable.
46
+ */
47
+ export function load(): Record<string, any> | null {
48
+ const p = checkpointPath();
49
+ if (!fs.existsSync(p)) {
50
+ return null;
51
+ }
52
+ try {
53
+ const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
54
+ return typeof data === 'object' && data !== null ? data : null;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Delete the checkpoint file.
62
+ */
63
+ export function clear(): void {
64
+ try {
65
+ const p = checkpointPath();
66
+ if (fs.existsSync(p)) {
67
+ fs.unlinkSync(p);
68
+ }
69
+ } catch {
70
+ // Ignore cleanup errors
71
+ }
72
+ }
73
+
74
+ // ── Serialization helpers ──
75
+
76
+ function serializeTask(t: any): Record<string, any> {
77
+ return {
78
+ id: t.id,
79
+ description: t.description,
80
+ assigned_to: t.assignedTo ?? t.assigned_to,
81
+ all_deps: t.allDeps ?? t.all_deps ?? [],
82
+ status: t.status?.value ?? t.status ?? 'unknown',
83
+ };
84
+ }
85
+
86
+ function serializeResult(r: any): Record<string, any> {
87
+ return {
88
+ id: r.id,
89
+ agent: r.agent,
90
+ description: r.description,
91
+ success: r.success,
92
+ content: r.content,
93
+ };
94
+ }
@@ -223,6 +223,14 @@ export interface SkyloomConfig {
223
223
  agents: Record<string, AgentConfig>;
224
224
  providers?: Record<string, ProviderEntry>;
225
225
  models?: Record<string, ModelEntry[]>;
226
+ /** Top-level default model chosen by the setup wizard (e.g. "deepseek-v4-flash"). */
227
+ default_model?: string;
228
+ /** Top-level default provider chosen by the setup wizard. */
229
+ default_provider?: string;
230
+ /** LLM defaults block from default.yaml / user config (snake_case keys). */
231
+ llm?: Record<string, any>;
232
+ /** Other passthrough top-level config (memory, workspace, cli, mcp, plugins, tts…). */
233
+ [key: string]: any;
226
234
  }
227
235
 
228
236
  /**
@@ -261,18 +269,26 @@ export function mergeConfigs(defaultCfg: SkyloomConfig, userCfg: SkyloomConfig |
261
269
  return defaultCfg;
262
270
  }
263
271
 
272
+ // Preserve all top-level keys (default_model, default_provider, llm, memory,
273
+ // workspace, …) with the user winning, then deep-merge the known sub-objects.
264
274
  return {
275
+ ...defaultCfg,
276
+ ...userCfg,
265
277
  agents: {
266
278
  ...defaultCfg.agents,
267
279
  ...userCfg.agents,
268
280
  },
269
281
  providers: {
270
- ...defaultCfg.providers,
271
- ...userCfg.providers,
282
+ ...(defaultCfg.providers || {}),
283
+ ...(userCfg.providers || {}),
272
284
  },
273
285
  models: {
274
- ...defaultCfg.models,
275
- ...userCfg.models,
286
+ ...(defaultCfg.models || {}),
287
+ ...(userCfg.models || {}),
288
+ },
289
+ llm: {
290
+ ...(defaultCfg.llm || {}),
291
+ ...(userCfg.llm || {}),
276
292
  },
277
293
  };
278
294
  }
@@ -1,104 +1,104 @@
1
- /**
2
- * 资源估算模块 — Token & time budget estimation for task planning.
3
- *
4
- * Helps Snow and other planning agents estimate the cost of
5
- * proposed sub-tasks before committing to execution.
6
- */
7
-
8
- import type { Task } from "./agent";
9
-
10
- /* ═══════════════════════════════════════
11
- Token estimation
12
- ═══════════════════════════════════════ */
13
- const CJK_REGEX = /[一-鿿぀-ゟ가-힯㐀-䶿]/g;
14
-
15
- /** Estimate tokens for a given text (CJK ~2 each, ASCII ~4 chars each). */
16
- export function estimateTokens(text: string): number {
17
- const cjk = (text.match(CJK_REGEX) || []).length;
18
- return cjk * 2 + Math.ceil((text.length - cjk) / 4);
19
- }
20
-
21
- /* ═══════════════════════════════════════
22
- Per-task-type cost estimates
23
- ═══════════════════════════════════════ */
24
- const TASK_TYPE_PATTERNS: Array<[RegExp, number, number]> = [
25
- // [pattern, estimated tokens, estimated tools]
26
- [/read|read_file|grep|search|查|搜索|list/i, 2000, 2],
27
- [/write|write_file|生成|写|create|implement/i, 4000, 5],
28
- [/edit|edit_file|改|修改|fix|修复/i, 3000, 3],
29
- [/delete|delete_file|删|rm/i, 1500, 2],
30
- [/deploy|部署|publish|发布|release/i, 8000, 8],
31
- [/review|审查|audit|审计|scan|扫描/i, 5000, 4],
32
- [/test|测试|run_test|coverage/i, 3000, 3],
33
- [/research|研究|调研|analyze|分析/i, 6000, 4],
34
- [/orchestrate|编排|multi-step|多步/i, 12000, 10],
35
- ];
36
-
37
- /** Estimate cost for a single task description. */
38
- export function estimateTaskCost(description: string): { tokens: number; tools: number; timeSeconds: number } {
39
- let tokens = 2000; // base
40
- let tools = 2; // base
41
- for (const [pattern, t, tc] of TASK_TYPE_PATTERNS) {
42
- if (pattern.test(description)) { tokens = Math.max(tokens, t); tools = Math.max(tools, tc); }
43
- }
44
-
45
- // Time estimate: ~0.5s per tool call + 2s per 1k tokens
46
- const timeSeconds = (tokens / 1000) * 2 + tools * 0.5 + 2;
47
- return { tokens, tools, timeSeconds };
48
- }
49
-
50
- /* ═══════════════════════════════════════
51
- Task plan cost summary
52
- ═══════════════════════════════════════ */
53
- export interface PlanEstimate {
54
- totalTokens: number;
55
- totalTools: number;
56
- totalTimeSeconds: number;
57
- perTask: Array<{ id: string; tokens: number; tools: number; time: number }>;
58
- warnings: string[];
59
- }
60
-
61
- export function estimateTaskPlan(tasks: Task[]): PlanEstimate {
62
- const perTask: PlanEstimate["perTask"] = [];
63
- let totalTokens = 500; // system prompt overhead
64
- let totalTools = 0;
65
- let totalTime = 5; // init overhead
66
- const warnings: string[] = [];
67
-
68
- for (const t of tasks) {
69
- const est = estimateTaskCost(t.description);
70
- perTask.push({ id: t.id, tokens: est.tokens, tools: est.tools, time: est.timeSeconds });
71
- totalTokens += est.tokens;
72
- totalTools += est.tools;
73
- totalTime += est.timeSeconds;
74
-
75
- if (est.timeSeconds > 60) warnings.push(`Task ${t.id} may take >${Math.round(est.timeSeconds)}s`);
76
- if (est.tools > 10) warnings.push(`Task ${t.id} uses many tool calls (${est.tools})`);
77
- }
78
-
79
- if (totalTokens > 64000) warnings.push(`Total token estimate (${totalTokens}) exceeds typical context window`);
80
- if (totalTime > 120) warnings.push(`Estimated total time (${Math.round(totalTime)}s) is significant`);
81
- if (tasks.length > 6) warnings.push(`Large number of sub-tasks (${tasks.length}) — consider merging simpler ones`);
82
-
83
- return { totalTokens, totalTools, totalTimeSeconds: Math.round(totalTime), perTask, warnings };
84
- }
85
-
86
- /* ═══════════════════════════════════════
87
- Format estimate for display
88
- ═══════════════════════════════════════ */
89
- export function formatPlanEstimate(est: PlanEstimate): string {
90
- const lines: string[] = [
91
- `## Plan Estimate`,
92
- `| Task | Tokens | Tools | Time |`,
93
- `|------|--------|-------|------|`,
94
- ...est.perTask.map(t => `| ${t.id} | ${t.tokens} | ${t.tools} | ${t.time.toFixed(0)}s |`),
95
- `| **Total** | **${est.totalTokens}** | **${est.totalTools}** | **${est.totalTimeSeconds}s** |`,
96
- ];
97
-
98
- if (est.warnings.length > 0) {
99
- lines.push("", "### Warnings");
100
- for (const w of est.warnings) lines.push(`- ⚠ ${w}`);
101
- }
102
-
103
- return lines.join("\n");
104
- }
1
+ /**
2
+ * 资源估算模块 — Token & time budget estimation for task planning.
3
+ *
4
+ * Helps Snow and other planning agents estimate the cost of
5
+ * proposed sub-tasks before committing to execution.
6
+ */
7
+
8
+ import type { Task } from "./agent";
9
+
10
+ /* ═══════════════════════════════════════
11
+ Token estimation
12
+ ═══════════════════════════════════════ */
13
+ const CJK_REGEX = /[一-鿿぀-ゟ가-힯㐀-䶿]/g;
14
+
15
+ /** Estimate tokens for a given text (CJK ~2 each, ASCII ~4 chars each). */
16
+ export function estimateTokens(text: string): number {
17
+ const cjk = (text.match(CJK_REGEX) || []).length;
18
+ return cjk * 2 + Math.ceil((text.length - cjk) / 4);
19
+ }
20
+
21
+ /* ═══════════════════════════════════════
22
+ Per-task-type cost estimates
23
+ ═══════════════════════════════════════ */
24
+ const TASK_TYPE_PATTERNS: Array<[RegExp, number, number]> = [
25
+ // [pattern, estimated tokens, estimated tools]
26
+ [/read|read_file|grep|search|查|搜索|list/i, 2000, 2],
27
+ [/write|write_file|生成|写|create|implement/i, 4000, 5],
28
+ [/edit|edit_file|改|修改|fix|修复/i, 3000, 3],
29
+ [/delete|delete_file|删|rm/i, 1500, 2],
30
+ [/deploy|部署|publish|发布|release/i, 8000, 8],
31
+ [/review|审查|audit|审计|scan|扫描/i, 5000, 4],
32
+ [/test|测试|run_test|coverage/i, 3000, 3],
33
+ [/research|研究|调研|analyze|分析/i, 6000, 4],
34
+ [/orchestrate|编排|multi-step|多步/i, 12000, 10],
35
+ ];
36
+
37
+ /** Estimate cost for a single task description. */
38
+ export function estimateTaskCost(description: string): { tokens: number; tools: number; timeSeconds: number } {
39
+ let tokens = 2000; // base
40
+ let tools = 2; // base
41
+ for (const [pattern, t, tc] of TASK_TYPE_PATTERNS) {
42
+ if (pattern.test(description)) { tokens = Math.max(tokens, t); tools = Math.max(tools, tc); }
43
+ }
44
+
45
+ // Time estimate: ~0.5s per tool call + 2s per 1k tokens
46
+ const timeSeconds = (tokens / 1000) * 2 + tools * 0.5 + 2;
47
+ return { tokens, tools, timeSeconds };
48
+ }
49
+
50
+ /* ═══════════════════════════════════════
51
+ Task plan cost summary
52
+ ═══════════════════════════════════════ */
53
+ export interface PlanEstimate {
54
+ totalTokens: number;
55
+ totalTools: number;
56
+ totalTimeSeconds: number;
57
+ perTask: Array<{ id: string; tokens: number; tools: number; time: number }>;
58
+ warnings: string[];
59
+ }
60
+
61
+ export function estimateTaskPlan(tasks: Task[]): PlanEstimate {
62
+ const perTask: PlanEstimate["perTask"] = [];
63
+ let totalTokens = 500; // system prompt overhead
64
+ let totalTools = 0;
65
+ let totalTime = 5; // init overhead
66
+ const warnings: string[] = [];
67
+
68
+ for (const t of tasks) {
69
+ const est = estimateTaskCost(t.description);
70
+ perTask.push({ id: t.id, tokens: est.tokens, tools: est.tools, time: est.timeSeconds });
71
+ totalTokens += est.tokens;
72
+ totalTools += est.tools;
73
+ totalTime += est.timeSeconds;
74
+
75
+ if (est.timeSeconds > 60) warnings.push(`Task ${t.id} may take >${Math.round(est.timeSeconds)}s`);
76
+ if (est.tools > 10) warnings.push(`Task ${t.id} uses many tool calls (${est.tools})`);
77
+ }
78
+
79
+ if (totalTokens > 64000) warnings.push(`Total token estimate (${totalTokens}) exceeds typical context window`);
80
+ if (totalTime > 120) warnings.push(`Estimated total time (${Math.round(totalTime)}s) is significant`);
81
+ if (tasks.length > 6) warnings.push(`Large number of sub-tasks (${tasks.length}) — consider merging simpler ones`);
82
+
83
+ return { totalTokens, totalTools, totalTimeSeconds: Math.round(totalTime), perTask, warnings };
84
+ }
85
+
86
+ /* ═══════════════════════════════════════
87
+ Format estimate for display
88
+ ═══════════════════════════════════════ */
89
+ export function formatPlanEstimate(est: PlanEstimate): string {
90
+ const lines: string[] = [
91
+ `## Plan Estimate`,
92
+ `| Task | Tokens | Tools | Time |`,
93
+ `|------|--------|-------|------|`,
94
+ ...est.perTask.map(t => `| ${t.id} | ${t.tokens} | ${t.tools} | ${t.time.toFixed(0)}s |`),
95
+ `| **Total** | **${est.totalTokens}** | **${est.totalTools}** | **${est.totalTimeSeconds}s** |`,
96
+ ];
97
+
98
+ if (est.warnings.length > 0) {
99
+ lines.push("", "### Warnings");
100
+ for (const w of est.warnings) lines.push(`- ⚠ ${w}`);
101
+ }
102
+
103
+ return lines.join("\n");
104
+ }