skyloom 1.4.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.
- package/.github/workflows/ci.yml +36 -0
- package/CONVERSION_PLAN.md +191 -0
- package/README.md +67 -0
- package/dist/agents/dew.d.ts +15 -0
- package/dist/agents/dew.d.ts.map +1 -0
- package/dist/agents/dew.js +74 -0
- package/dist/agents/dew.js.map +1 -0
- package/dist/agents/fair.d.ts +15 -0
- package/dist/agents/fair.d.ts.map +1 -0
- package/dist/agents/fair.js +106 -0
- package/dist/agents/fair.js.map +1 -0
- package/dist/agents/fog.d.ts +15 -0
- package/dist/agents/fog.d.ts.map +1 -0
- package/dist/agents/fog.js +52 -0
- package/dist/agents/fog.js.map +1 -0
- package/dist/agents/frost.d.ts +15 -0
- package/dist/agents/frost.d.ts.map +1 -0
- package/dist/agents/frost.js +54 -0
- package/dist/agents/frost.js.map +1 -0
- package/dist/agents/rain.d.ts +15 -0
- package/dist/agents/rain.d.ts.map +1 -0
- package/dist/agents/rain.js +54 -0
- package/dist/agents/rain.js.map +1 -0
- package/dist/agents/snow.d.ts +27 -0
- package/dist/agents/snow.d.ts.map +1 -0
- package/dist/agents/snow.js +226 -0
- package/dist/agents/snow.js.map +1 -0
- package/dist/cli/main.d.ts +7 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +402 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/mode.d.ts +17 -0
- package/dist/cli/mode.d.ts.map +1 -0
- package/dist/cli/mode.js +56 -0
- package/dist/cli/mode.js.map +1 -0
- package/dist/core/agent.d.ts +174 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +1332 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/agent_helpers.d.ts +51 -0
- package/dist/core/agent_helpers.d.ts.map +1 -0
- package/dist/core/agent_helpers.js +477 -0
- package/dist/core/agent_helpers.js.map +1 -0
- package/dist/core/bus.d.ts +99 -0
- package/dist/core/bus.d.ts.map +1 -0
- package/dist/core/bus.js +191 -0
- package/dist/core/bus.js.map +1 -0
- package/dist/core/cache.d.ts +63 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +121 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/checkpoint.d.ts +19 -0
- package/dist/core/checkpoint.d.ts.map +1 -0
- package/dist/core/checkpoint.js +120 -0
- package/dist/core/checkpoint.js.map +1 -0
- package/dist/core/circuit_breaker.d.ts +46 -0
- package/dist/core/circuit_breaker.d.ts.map +1 -0
- package/dist/core/circuit_breaker.js +99 -0
- package/dist/core/circuit_breaker.js.map +1 -0
- package/dist/core/config.d.ts +97 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +281 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/constants.d.ts +78 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/constants.js +84 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/factory.d.ts +63 -0
- package/dist/core/factory.d.ts.map +1 -0
- package/dist/core/factory.js +537 -0
- package/dist/core/factory.js.map +1 -0
- package/dist/core/icons.d.ts +28 -0
- package/dist/core/icons.d.ts.map +1 -0
- package/dist/core/icons.js +86 -0
- package/dist/core/icons.js.map +1 -0
- package/dist/core/index.d.ts +29 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +54 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/llm.d.ts +121 -0
- package/dist/core/llm.d.ts.map +1 -0
- package/dist/core/llm.js +532 -0
- package/dist/core/llm.js.map +1 -0
- package/dist/core/logger.d.ts +57 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +122 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/mcp.d.ts +190 -0
- package/dist/core/mcp.d.ts.map +1 -0
- package/dist/core/mcp.js +822 -0
- package/dist/core/mcp.js.map +1 -0
- package/dist/core/mcp_server.d.ts +26 -0
- package/dist/core/mcp_server.d.ts.map +1 -0
- package/dist/core/mcp_server.js +211 -0
- package/dist/core/mcp_server.js.map +1 -0
- package/dist/core/memory.d.ts +190 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +988 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/middleware.d.ts +114 -0
- package/dist/core/middleware.d.ts.map +1 -0
- package/dist/core/middleware.js +248 -0
- package/dist/core/middleware.js.map +1 -0
- package/dist/core/pipelines.d.ts +87 -0
- package/dist/core/pipelines.d.ts.map +1 -0
- package/dist/core/pipelines.js +301 -0
- package/dist/core/pipelines.js.map +1 -0
- package/dist/core/profile.d.ts +23 -0
- package/dist/core/profile.d.ts.map +1 -0
- package/dist/core/profile.js +289 -0
- package/dist/core/profile.js.map +1 -0
- package/dist/core/router.d.ts +24 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/router.js +111 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/schemas.d.ts +82 -0
- package/dist/core/schemas.d.ts.map +1 -0
- package/dist/core/schemas.js +200 -0
- package/dist/core/schemas.js.map +1 -0
- package/dist/core/semantic.d.ts +92 -0
- package/dist/core/semantic.d.ts.map +1 -0
- package/dist/core/semantic.js +175 -0
- package/dist/core/semantic.js.map +1 -0
- package/dist/core/skill.d.ts +68 -0
- package/dist/core/skill.d.ts.map +1 -0
- package/dist/core/skill.js +350 -0
- package/dist/core/skill.js.map +1 -0
- package/dist/core/tool.d.ts +99 -0
- package/dist/core/tool.d.ts.map +1 -0
- package/dist/core/tool.js +341 -0
- package/dist/core/tool.js.map +1 -0
- package/dist/core/tool_router.d.ts +29 -0
- package/dist/core/tool_router.d.ts.map +1 -0
- package/dist/core/tool_router.js +172 -0
- package/dist/core/tool_router.js.map +1 -0
- package/dist/core/workspace.d.ts +48 -0
- package/dist/core/workspace.d.ts.map +1 -0
- package/dist/core/workspace.js +179 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/plugins/loader.d.ts +17 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +96 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/skills/loader.d.ts +9 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +78 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/tools/builtin.d.ts +10 -0
- package/dist/tools/builtin.d.ts.map +1 -0
- package/dist/tools/builtin.js +414 -0
- package/dist/tools/builtin.js.map +1 -0
- package/dist/tools/computer.d.ts +12 -0
- package/dist/tools/computer.d.ts.map +1 -0
- package/dist/tools/computer.js +326 -0
- package/dist/tools/computer.js.map +1 -0
- package/dist/tools/delegate.d.ts +10 -0
- package/dist/tools/delegate.d.ts.map +1 -0
- package/dist/tools/delegate.js +45 -0
- package/dist/tools/delegate.js.map +1 -0
- package/dist/web/server.d.ts +5 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +647 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web/tts.d.ts +33 -0
- package/dist/web/tts.d.ts.map +1 -0
- package/dist/web/tts.js +69 -0
- package/dist/web/tts.js.map +1 -0
- package/package.json +60 -0
- package/scripts/install.js +48 -0
- package/scripts/link.js +10 -0
- package/setup.bat +79 -0
- package/skill-test-ty2fOA/test.md +10 -0
- package/src/agents/dew.ts +70 -0
- package/src/agents/fair.ts +102 -0
- package/src/agents/fog.ts +48 -0
- package/src/agents/frost.ts +50 -0
- package/src/agents/rain.ts +50 -0
- package/src/agents/snow.ts +239 -0
- package/src/cli/main.ts +405 -0
- package/src/cli/mode.ts +58 -0
- package/src/core/agent.ts +1506 -0
- package/src/core/agent_helpers.ts +461 -0
- package/src/core/bus.ts +221 -0
- package/src/core/cache.ts +153 -0
- package/src/core/checkpoint.ts +94 -0
- package/src/core/circuit_breaker.ts +119 -0
- package/src/core/config.ts +341 -0
- package/src/core/constants.ts +95 -0
- package/src/core/factory.ts +627 -0
- package/src/core/icons.ts +53 -0
- package/src/core/index.ts +31 -0
- package/src/core/llm.ts +724 -0
- package/src/core/logger.ts +144 -0
- package/src/core/mcp.ts +953 -0
- package/src/core/mcp_server.ts +176 -0
- package/src/core/memory.ts +1169 -0
- package/src/core/middleware.ts +350 -0
- package/src/core/pipelines.ts +424 -0
- package/src/core/profile.ts +255 -0
- package/src/core/router.ts +124 -0
- package/src/core/schemas.ts +282 -0
- package/src/core/semantic.ts +211 -0
- package/src/core/skill.ts +342 -0
- package/src/core/tool.ts +427 -0
- package/src/core/tool_router.ts +193 -0
- package/src/core/workspace.ts +150 -0
- package/src/plugins/loader.ts +66 -0
- package/src/skills/loader.ts +46 -0
- package/src/sql.js.d.ts +29 -0
- package/src/tools/builtin.ts +382 -0
- package/src/tools/computer.ts +269 -0
- package/src/tools/delegate.ts +49 -0
- package/src/web/server.ts +634 -0
- package/src/web/tts.ts +93 -0
- package/tests/bus.test.ts +121 -0
- package/tests/icons.test.ts +45 -0
- package/tests/router.test.ts +86 -0
- package/tests/schemas.test.ts +51 -0
- package/tests/semantic.test.ts +83 -0
- package/tests/setup.ts +10 -0
- package/tests/skill.test.ts +172 -0
- package/tests/tool.test.ts +108 -0
- package/tests/tool_router.test.ts +71 -0
- package/tsconfig.json +37 -0
- package/vitest.config.ts +17 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 雪 (Snow) — 架构规划型全能 Agent.
|
|
3
|
+
* A general-purpose agent specializing in architecture and planning.
|
|
4
|
+
* Also handles task decomposition and orchestration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BaseAgent, Task } from '../core/agent';
|
|
8
|
+
|
|
9
|
+
// Valid agents for task assignment (fair is independent, not part of orchestration)
|
|
10
|
+
const VALID_AGENTS = new Set(['fog', 'rain', 'frost', 'snow', 'dew']);
|
|
11
|
+
|
|
12
|
+
export class SnowAgent extends BaseAgent {
|
|
13
|
+
name = 'snow';
|
|
14
|
+
displayName = '雪';
|
|
15
|
+
emoji = '❉';
|
|
16
|
+
specialty = '架构规划';
|
|
17
|
+
skillNames = ['task_planner', 'arch_designer', 'workflow_designer', 'self_evolve'];
|
|
18
|
+
|
|
19
|
+
systemPrompt = `你是 Skyloom 的「雪」。
|
|
20
|
+
|
|
21
|
+
你是全能 agent —— 代码、写作、审查、部署、规划、研究,你都能独立交付。
|
|
22
|
+
你的特质是「全局视野」:先看清结构、依赖、顺序、风险,再动手。
|
|
23
|
+
混乱的需求经你一拆,就变成清晰的步骤树。这是你看世界的方式,不仅是你做编排时才用。
|
|
24
|
+
|
|
25
|
+
## 协作
|
|
26
|
+
|
|
27
|
+
90% 的事自己做完。只有任务跨 5+ 领域、上下文塞不下、或需要多轮独立审查时,才调其他 agent。
|
|
28
|
+
调用时给足上下文,拿到结果整合成完整答复,用户不需要感知协作过程。
|
|
29
|
+
|
|
30
|
+
## 风格
|
|
31
|
+
|
|
32
|
+
像雪一样静默但覆盖一切 —— 结构清晰,考虑周全。
|
|
33
|
+
- 大任务先给整体框架,再深入
|
|
34
|
+
- 标注依赖、风险、预计工作量
|
|
35
|
+
- 规划:框架先于理由
|
|
36
|
+
- 执行:按优先级推进,完成后汇总
|
|
37
|
+
- 让人感觉「一切都在掌控之中」`;
|
|
38
|
+
|
|
39
|
+
systemPromptEn = `You are "Snow" of Skyloom.
|
|
40
|
+
|
|
41
|
+
A general-purpose agent — code, writing, review, ops, planning, research — you ship anything alone.
|
|
42
|
+
Your nature: see the whole. Structure, dependencies, sequence, risk — all before the first move.
|
|
43
|
+
Messy requirements come out as clear step trees. That's how you see, not just how you orchestrate.
|
|
44
|
+
|
|
45
|
+
## Collaboration
|
|
46
|
+
Do 90% alone. Delegate only when 5+ domains, context overflow, or multi-round review is needed.
|
|
47
|
+
Pass full context; synthesize the result yourself.
|
|
48
|
+
|
|
49
|
+
## Style
|
|
50
|
+
Like snow — silent but all-covering. Clear structure, thorough consideration.
|
|
51
|
+
- Big work: framework first, then detail
|
|
52
|
+
- Note dependencies, risks, effort estimates
|
|
53
|
+
- Planning: structure before justification
|
|
54
|
+
- Execution: prioritize, deliver, summarize
|
|
55
|
+
- Leaves the user feeling "this is under control"`;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Decompose a goal into tasks and dispatch to agents.
|
|
59
|
+
*/
|
|
60
|
+
async orchestrate(goal: string): Promise<Task[]> {
|
|
61
|
+
const prompt = `请将以下目标分解为子任务,并分配给合适的 Agent。
|
|
62
|
+
|
|
63
|
+
目标: ${goal}
|
|
64
|
+
|
|
65
|
+
请严格按照以下 JSON Schema 输出,不要包含其他内容:
|
|
66
|
+
{"goal": "目标描述", "steps": [
|
|
67
|
+
{"id": "1", "description": "任务描述", "agent": "fog"},
|
|
68
|
+
{"id": "2", "description": "后续任务", "agent": "rain", "depends_on": ["1"]}
|
|
69
|
+
]}
|
|
70
|
+
|
|
71
|
+
可用 Agent: fog(调研/搜索), rain(代码生成/写作), frost(审查/安全), dew(部署/运维)
|
|
72
|
+
注意:fair 是独立的情感陪伴 agent,**不参与任何任务编排**,绝不要分配给她。
|
|
73
|
+
注意:如果任务有先后依赖关系,必须用 depends_on 字段标出。
|
|
74
|
+
不要使用工具,直接输出 JSON 即可。`;
|
|
75
|
+
|
|
76
|
+
this.memory.addMessage('user', prompt);
|
|
77
|
+
const response = await this.llmLoop();
|
|
78
|
+
this.memory.addMessage('assistant', response.content);
|
|
79
|
+
|
|
80
|
+
// Try schema-validated parsing first
|
|
81
|
+
try {
|
|
82
|
+
const { parseTaskPlan } = require('../core/schemas');
|
|
83
|
+
const parsed = parseTaskPlan(response.content);
|
|
84
|
+
if (parsed && parsed.steps && parsed.steps.length > 0) {
|
|
85
|
+
return this.schemaToTasks(parsed, goal);
|
|
86
|
+
}
|
|
87
|
+
} catch { /* fallthrough */ }
|
|
88
|
+
|
|
89
|
+
// Fallback to heuristic parsing
|
|
90
|
+
return this.parseTaskPlan(response.content, goal);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Produce additional tasks that close the gap reported by the judge.
|
|
95
|
+
*/
|
|
96
|
+
async replanForMissing(
|
|
97
|
+
goal: string,
|
|
98
|
+
priorResults: any[],
|
|
99
|
+
missing: string,
|
|
100
|
+
existingIds?: Set<string>
|
|
101
|
+
): Promise<Task[]> {
|
|
102
|
+
const usedIds = existingIds || new Set<string>();
|
|
103
|
+
const priorLines = priorResults.map(r => {
|
|
104
|
+
const status = r.success ? '成功' : '失败';
|
|
105
|
+
return `- task ${r.id} (${r.agent}, ${status}): ${(r.content || '').slice(0, 200)}`;
|
|
106
|
+
});
|
|
107
|
+
const priorText = priorLines.length > 0 ? priorLines.join('\n') : '(none yet)';
|
|
108
|
+
|
|
109
|
+
// Identify failing agents
|
|
110
|
+
const failingAgents = new Set(
|
|
111
|
+
priorResults
|
|
112
|
+
.filter(r => !r.success || !r.content || r.content.trim().length < 16)
|
|
113
|
+
.map(r => r.agent)
|
|
114
|
+
.filter(Boolean)
|
|
115
|
+
);
|
|
116
|
+
const failingHint = failingAgents.size > 0
|
|
117
|
+
? `\n\n## 已证明无效的 agent\n${[...failingAgents].sort().join(', ')} 已在上一轮返回占位/无交付物。\n**禁止把同类任务再交给以上 agent**。`
|
|
118
|
+
: '';
|
|
119
|
+
|
|
120
|
+
const prompt = `之前的子任务执行后,验收员发现还有缺口。请仅针对**缺失的部分**追加新的子任务。
|
|
121
|
+
|
|
122
|
+
## 原目标
|
|
123
|
+
${goal}
|
|
124
|
+
|
|
125
|
+
## 已执行子任务
|
|
126
|
+
${priorText}
|
|
127
|
+
|
|
128
|
+
## 缺口(验收员报告)
|
|
129
|
+
${missing}
|
|
130
|
+
|
|
131
|
+
## 已使用的 task id(必须避开)
|
|
132
|
+
${usedIds.size > 0 ? [...usedIds].sort().join(', ') : '(none)'}
|
|
133
|
+
${failingHint}
|
|
134
|
+
|
|
135
|
+
请输出新任务的 JSON 计划:
|
|
136
|
+
{"steps": [{"id": "新id", "agent": "fog|rain|frost|dew", "description": "具体任务", "depends_on": ["可选已完成任务id"]}]}
|
|
137
|
+
|
|
138
|
+
约束:
|
|
139
|
+
- 只输出新增任务,不要重复已完成的;id 必须避开上面的列表
|
|
140
|
+
- 控制在 2 个新任务以内(精简优先)
|
|
141
|
+
- 只输出 JSON,无其他文本`;
|
|
142
|
+
|
|
143
|
+
this.memory.addMessage('user', prompt);
|
|
144
|
+
const response = await this.llmLoop();
|
|
145
|
+
this.memory.addMessage('assistant', response.content);
|
|
146
|
+
|
|
147
|
+
let newTasks: Task[];
|
|
148
|
+
try {
|
|
149
|
+
const { parseTaskPlan } = require('../core/schemas');
|
|
150
|
+
const parsed = parseTaskPlan(response.content);
|
|
151
|
+
if (parsed && parsed.steps) {
|
|
152
|
+
newTasks = this.schemaToTasks(parsed, goal);
|
|
153
|
+
} else {
|
|
154
|
+
newTasks = this.parseTaskPlan(response.content, goal);
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
newTasks = this.parseTaskPlan(response.content, goal);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Deduplicate IDs
|
|
161
|
+
const deduped: Task[] = [];
|
|
162
|
+
for (const t of newTasks) {
|
|
163
|
+
if (usedIds.has(t.id)) {
|
|
164
|
+
t.id = `r${usedIds.size + deduped.length + 1}_${t.id}`;
|
|
165
|
+
}
|
|
166
|
+
deduped.push(t);
|
|
167
|
+
usedIds.add(t.id);
|
|
168
|
+
}
|
|
169
|
+
return deduped;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private schemaToTasks(plan: any, goal: string): Task[] {
|
|
173
|
+
const tasks: Task[] = [];
|
|
174
|
+
for (const step of plan.steps || []) {
|
|
175
|
+
const sid = String(step.id);
|
|
176
|
+
const depends: string[] = step.depends_on || [];
|
|
177
|
+
const agent = VALID_AGENTS.has(step.agent) ? step.agent : 'rain';
|
|
178
|
+
tasks.push(new Task({
|
|
179
|
+
id: sid,
|
|
180
|
+
description: step.description,
|
|
181
|
+
assignedTo: agent,
|
|
182
|
+
parentId: depends.length > 0 ? depends[0] : null,
|
|
183
|
+
dependsOn: depends,
|
|
184
|
+
metadata: { goal, priority: step.priority || 'medium' },
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
return tasks;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private parseTaskPlan(content: string, goal: string): Task[] {
|
|
191
|
+
// Try to extract JSON from markdown code blocks
|
|
192
|
+
const jsonBlockPatterns = [
|
|
193
|
+
/```json\s*\n(.*?)\n```/s,
|
|
194
|
+
/```\s*\n(\{.*?\})\n```/s,
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
for (const pattern of jsonBlockPatterns) {
|
|
198
|
+
const match = content.match(pattern);
|
|
199
|
+
if (match) {
|
|
200
|
+
try {
|
|
201
|
+
const plan = JSON.parse(match[1].trim());
|
|
202
|
+
const tasks = this.planToTasks(plan, goal);
|
|
203
|
+
if (tasks.length > 0) return tasks;
|
|
204
|
+
} catch { /* continue */ }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Try to find raw JSON in response
|
|
209
|
+
try {
|
|
210
|
+
const start = content.indexOf('{');
|
|
211
|
+
const end = content.lastIndexOf('}') + 1;
|
|
212
|
+
if (start >= 0 && end > start) {
|
|
213
|
+
const plan = JSON.parse(content.slice(start, end));
|
|
214
|
+
const tasks = this.planToTasks(plan, goal);
|
|
215
|
+
if (tasks.length > 0) return tasks;
|
|
216
|
+
}
|
|
217
|
+
} catch { /* continue */ }
|
|
218
|
+
|
|
219
|
+
// Fallback: single task for rain
|
|
220
|
+
return [new Task({ id: '1', description: goal, assignedTo: 'rain', metadata: { goal } })];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private planToTasks(plan: Record<string, any>, goal: string): Task[] {
|
|
224
|
+
const tasks: Task[] = [];
|
|
225
|
+
for (const step of (plan.steps || []) as any[]) {
|
|
226
|
+
const agent = VALID_AGENTS.has(step.agent) ? step.agent : 'rain';
|
|
227
|
+
const depends: string[] = Array.isArray(step.depends_on) ? step.depends_on : [];
|
|
228
|
+
tasks.push(new Task({
|
|
229
|
+
id: String(step.id || tasks.length + 1),
|
|
230
|
+
description: step.description || '',
|
|
231
|
+
assignedTo: agent,
|
|
232
|
+
parentId: depends.length > 0 ? depends[0] : null,
|
|
233
|
+
dependsOn: depends,
|
|
234
|
+
metadata: { goal, priority: step.priority || 'medium' },
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
return tasks;
|
|
238
|
+
}
|
|
239
|
+
}
|
package/src/cli/main.ts
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI interface for Skyloom — terminal agent product.
|
|
5
|
+
* Uses Commander.js for command routing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as readline from 'readline';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { createSystemContext, orchestrateTask } from '../core/factory';
|
|
13
|
+
import { loadConfig, USER_CONFIG_DIR } from '../core/config';
|
|
14
|
+
import { classify } from '../core/router';
|
|
15
|
+
import { InteractiveMode, ModeController } from './mode';
|
|
16
|
+
|
|
17
|
+
const MODE = new ModeController();
|
|
18
|
+
const VERSION = '1.4.0';
|
|
19
|
+
|
|
20
|
+
const program = new Command();
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.name('sky')
|
|
24
|
+
.description('Skyloom CLI — multi-agent orchestration framework')
|
|
25
|
+
.version(VERSION);
|
|
26
|
+
|
|
27
|
+
// ── Chat command ──
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('chat')
|
|
31
|
+
.description('Start interactive chat with an agent')
|
|
32
|
+
.argument('[agent]', 'Agent name (fog, rain, frost, snow, dew, fair)', 'fog')
|
|
33
|
+
.option('-m, --model <model>', 'Model to use')
|
|
34
|
+
.action(async (agentName: string, options: { model?: string }) => {
|
|
35
|
+
try {
|
|
36
|
+
await interactiveChat(agentName, options.model);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error(chalk.red(`Error: ${e}`));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── Task command ──
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command('task')
|
|
47
|
+
.description('Execute a multi-agent orchestration task')
|
|
48
|
+
.argument('[goal]', 'Task goal description')
|
|
49
|
+
.option('-r, --resume', 'Resume from checkpoint')
|
|
50
|
+
.action(async (goal?: string, options?: { resume?: boolean }) => {
|
|
51
|
+
if (!goal) {
|
|
52
|
+
console.log(chalk.yellow('Please provide a task goal. Usage: sky task "<goal>"'));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
await runTask(goal, options?.resume);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error(chalk.red(`Error: ${e}`));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ── Web command ──
|
|
63
|
+
|
|
64
|
+
program
|
|
65
|
+
.command('web')
|
|
66
|
+
.description('Start web server')
|
|
67
|
+
.option('-p, --port <port>', 'Port to listen on', '3000')
|
|
68
|
+
.action(async (options: { port?: string }) => {
|
|
69
|
+
try {
|
|
70
|
+
// Dynamic import to avoid loading express when not needed
|
|
71
|
+
const { startWebServer } = await import('../web/server');
|
|
72
|
+
const port = parseInt(options.port || '3000', 10);
|
|
73
|
+
await startWebServer(port);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error(chalk.red(`Web server error: ${e}`));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ── MCP command ──
|
|
80
|
+
|
|
81
|
+
program
|
|
82
|
+
.command('mcp')
|
|
83
|
+
.description('Start MCP server (stdio JSON-RPC for Claude Desktop etc.)')
|
|
84
|
+
.action(async () => {
|
|
85
|
+
try {
|
|
86
|
+
console.error(chalk.cyan('Starting MCP server on stdio...'));
|
|
87
|
+
const { startMCPServer } = await import('../core/mcp_server');
|
|
88
|
+
await startMCPServer();
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error(chalk.red(`MCP server error: ${e}`));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ── Config command ──
|
|
96
|
+
|
|
97
|
+
program
|
|
98
|
+
.command('config')
|
|
99
|
+
.description('Show current configuration')
|
|
100
|
+
.action(() => {
|
|
101
|
+
const config = loadConfig();
|
|
102
|
+
console.log(chalk.bold('\nSkyloom Configuration'));
|
|
103
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
104
|
+
console.log(chalk.cyan('Config dir:'), USER_CONFIG_DIR);
|
|
105
|
+
console.log(chalk.cyan('Agents:'));
|
|
106
|
+
for (const [name, cfg] of Object.entries(config.agents || {})) {
|
|
107
|
+
console.log(` ${chalk.bold(name)}: ${cfg.model || 'default'}`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── Init / Setup command ──
|
|
112
|
+
|
|
113
|
+
program
|
|
114
|
+
.command('init')
|
|
115
|
+
.description('Initialize Skyloom configuration')
|
|
116
|
+
.action(() => {
|
|
117
|
+
const configDir = USER_CONFIG_DIR;
|
|
118
|
+
if (!fs.existsSync(configDir)) {
|
|
119
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
console.log(chalk.green(`✓ Initialized config at ${configDir}`));
|
|
122
|
+
console.log(chalk.dim('Edit config.yaml in that directory to configure agents and models.'));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── Version command ──
|
|
126
|
+
|
|
127
|
+
program
|
|
128
|
+
.command('version')
|
|
129
|
+
.description('Show version')
|
|
130
|
+
.action(() => {
|
|
131
|
+
console.log(`Skyloom v${VERSION}`);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── Interactive chat ──
|
|
135
|
+
|
|
136
|
+
async function interactiveChat(agentName: string, modelOverride?: string): Promise<void> {
|
|
137
|
+
const ctx = createSystemContext();
|
|
138
|
+
const agent = ctx.agentMap.get(agentName);
|
|
139
|
+
if (!agent) {
|
|
140
|
+
console.error(chalk.red(`Unknown agent: ${agentName}. Available: ${[...ctx.agentMap.keys()].join(', ')}`));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await agent.init();
|
|
145
|
+
const color = getAgentColor(agentName);
|
|
146
|
+
|
|
147
|
+
console.log();
|
|
148
|
+
console.log(chalk.cyan('≈ S K Y L O O M ≈'));
|
|
149
|
+
console.log(chalk.dim(`Agent: ${chalk.bold(agent.displayName)} · Model: ${modelOverride || 'default'}`));
|
|
150
|
+
console.log(chalk.dim('Type /help for commands, /quit to exit'));
|
|
151
|
+
console.log();
|
|
152
|
+
|
|
153
|
+
const rl = readline.createInterface({
|
|
154
|
+
input: process.stdin,
|
|
155
|
+
output: process.stdout,
|
|
156
|
+
prompt: '',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const inputHistory: string[] = [];
|
|
160
|
+
|
|
161
|
+
const processInput = async (input: string): Promise<void> => {
|
|
162
|
+
const cmd = input.trim();
|
|
163
|
+
if (!cmd) return;
|
|
164
|
+
|
|
165
|
+
// Slash commands
|
|
166
|
+
if (cmd === '/quit' || cmd === '/exit') {
|
|
167
|
+
rl.close();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (cmd === '/help') {
|
|
171
|
+
printHelp();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (cmd === '/clear') {
|
|
175
|
+
console.clear();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (cmd === '/status') {
|
|
179
|
+
const status = agent.getStatus();
|
|
180
|
+
console.log(chalk.bold('\nAgent Status'));
|
|
181
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
182
|
+
console.log(` ${chalk.cyan(status.displayName)} (${status.name})`);
|
|
183
|
+
console.log(` State: ${status.state}`);
|
|
184
|
+
console.log(` Specialty: ${status.specialty}`);
|
|
185
|
+
if (status.skills?.length) {
|
|
186
|
+
const active = status.skills.filter((s: any) => s.active).map((s: any) => s.name);
|
|
187
|
+
if (active.length) console.log(` Active skills: ${chalk.green(active.join(', '))}`);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (cmd === '/cost') {
|
|
192
|
+
const totalCost = ctx.llm.getTotalCost();
|
|
193
|
+
console.log(` Total cost: ${chalk.green(formatCost(totalCost))}`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (cmd === '/compact') {
|
|
197
|
+
const result = await agent.compact();
|
|
198
|
+
console.log(chalk.green(` ✓ ${result}`));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (cmd === '/version') {
|
|
202
|
+
console.log(` Skyloom v${VERSION}`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (cmd.startsWith('/model')) {
|
|
206
|
+
console.log(chalk.dim(' Model management: use config.yaml to change models'));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (cmd.startsWith('/task ')) {
|
|
210
|
+
const goal = cmd.slice(6).trim();
|
|
211
|
+
if (goal) {
|
|
212
|
+
console.log(chalk.cyan(`\n Orchestrating: ${goal}\n`));
|
|
213
|
+
await runTask(goal);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (cmd.startsWith('/')) {
|
|
218
|
+
printHelp();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Save to history
|
|
223
|
+
if (!inputHistory.includes(cmd)) {
|
|
224
|
+
inputHistory.push(cmd);
|
|
225
|
+
if (inputHistory.length > 50) inputHistory.shift();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Classify and route
|
|
229
|
+
const mode = MODE.current;
|
|
230
|
+
if (mode === InteractiveMode.PLAN) {
|
|
231
|
+
console.log(chalk.magenta('\n [PLAN mode] Routing to orchestrator...\n'));
|
|
232
|
+
await runTask(cmd);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const cls = classify(cmd);
|
|
237
|
+
if (cls === 'orchestrate' && mode !== InteractiveMode.AUTO) {
|
|
238
|
+
await runTask(cmd);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Single-agent chat
|
|
243
|
+
process.stdout.write(`\n ${chalk.cyan(agent.displayName)} ${chalk.dim('thinking...')}\n`);
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const response = await agent.chat(cmd);
|
|
247
|
+
process.stdout.write('\n');
|
|
248
|
+
console.log(chalk.white(response));
|
|
249
|
+
} catch (e) {
|
|
250
|
+
console.error(chalk.red(`\n Error: ${e}`));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// AUTO mode: continue if model signals more work
|
|
254
|
+
if (mode === InteractiveMode.AUTO) {
|
|
255
|
+
// Simple auto-continue check
|
|
256
|
+
const lastMsg = agent.memory.shortTerm[agent.memory.shortTerm.length - 1];
|
|
257
|
+
if (lastMsg && lastMsg.content && shouldAutoContinue(lastMsg.content)) {
|
|
258
|
+
process.stdout.write(chalk.yellow('\n [auto-continue]\n'));
|
|
259
|
+
// Re-trigger
|
|
260
|
+
try {
|
|
261
|
+
const response = await agent.chat('请继续完成');
|
|
262
|
+
process.stdout.write('\n');
|
|
263
|
+
console.log(chalk.white(response));
|
|
264
|
+
} catch (e) {
|
|
265
|
+
console.error(chalk.red(`\n Error: ${e}`));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
rl.on('line', async (line) => {
|
|
272
|
+
try {
|
|
273
|
+
await processInput(line);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
console.error(chalk.red(`Error: ${e}`));
|
|
276
|
+
}
|
|
277
|
+
rl.prompt();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
rl.on('close', () => {
|
|
281
|
+
console.log(chalk.dim('\n Session ended'));
|
|
282
|
+
ctx.closeAll().catch(() => {});
|
|
283
|
+
process.exit(0);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
rl.prompt();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function runTask(goal: string, resume?: boolean): Promise<void> {
|
|
290
|
+
const ctx = createSystemContext();
|
|
291
|
+
await ctx.initAll();
|
|
292
|
+
|
|
293
|
+
const [_tasks, results, summary] = await orchestrateTask(
|
|
294
|
+
goal,
|
|
295
|
+
ctx.agentMap,
|
|
296
|
+
null,
|
|
297
|
+
{
|
|
298
|
+
resultTruncate: 500,
|
|
299
|
+
maxTaskRetries: 3,
|
|
300
|
+
maxReplanRounds: 1,
|
|
301
|
+
resume,
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
console.log(chalk.bold('\n Task Results'));
|
|
306
|
+
console.log(chalk.dim(' ─'.repeat(30)));
|
|
307
|
+
|
|
308
|
+
for (const r of results) {
|
|
309
|
+
const status = r.success ? chalk.green('✓') : chalk.red('✗');
|
|
310
|
+
console.log(` ${status} ${chalk.cyan(r.agent)}: ${r.description.slice(0, 60)}...`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.log(chalk.bold('\n Summary'));
|
|
314
|
+
console.log(chalk.dim(' ─'.repeat(30)));
|
|
315
|
+
console.log(` ${summary.slice(0, 1000)}`);
|
|
316
|
+
console.log();
|
|
317
|
+
|
|
318
|
+
await ctx.closeAll();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function printHelp(): void {
|
|
322
|
+
console.log(chalk.bold('\n Commands'));
|
|
323
|
+
console.log(chalk.dim(' ─'.repeat(30)));
|
|
324
|
+
const cmds = [
|
|
325
|
+
['/help', 'Show this help'],
|
|
326
|
+
['/clear', 'Clear screen'],
|
|
327
|
+
['/status', 'Agent status'],
|
|
328
|
+
['/cost', 'Usage & cost'],
|
|
329
|
+
['/compact', 'Compress context'],
|
|
330
|
+
['/version', 'Version info'],
|
|
331
|
+
['/task <goal>', 'Multi-agent task'],
|
|
332
|
+
['/quit', 'Exit chat'],
|
|
333
|
+
['', ''],
|
|
334
|
+
['Switch agents:', ''],
|
|
335
|
+
['/fog', 'Fog — research'],
|
|
336
|
+
['/rain', 'Rain — codegen'],
|
|
337
|
+
['/frost', 'Frost — review'],
|
|
338
|
+
['/snow', 'Snow — planning'],
|
|
339
|
+
['/dew', 'Dew — devops'],
|
|
340
|
+
['/fair', 'Fair — companion'],
|
|
341
|
+
];
|
|
342
|
+
for (const [cmd, desc] of cmds) {
|
|
343
|
+
if (cmd) {
|
|
344
|
+
console.log(` ${chalk.cyan(cmd.padEnd(20))}${chalk.dim(desc)}`);
|
|
345
|
+
} else {
|
|
346
|
+
console.log();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
console.log();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function getAgentColor(name: string): string {
|
|
353
|
+
const colors: Record<string, string> = {
|
|
354
|
+
fog: 'bright_white', rain: 'blue', frost: 'cyan',
|
|
355
|
+
snow: 'bright_white', dew: 'green', fair: '#FFD700',
|
|
356
|
+
};
|
|
357
|
+
return colors[name] || 'white';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function formatCost(cost: number): string {
|
|
361
|
+
if (cost >= 1.0) return `$${cost.toFixed(2)}`;
|
|
362
|
+
if (cost >= 0.01) return `$${cost.toFixed(4)}`;
|
|
363
|
+
if (cost > 0.0) return `${(cost * 100).toFixed(2)}¢`;
|
|
364
|
+
return '$0';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function shouldAutoContinue(text: string): boolean {
|
|
368
|
+
const autoContinuePattern = /(?:接下来|下一步|下面我|然后我|接着|继续|next|let me\s|I'[vl]l\s)/i;
|
|
369
|
+
const autoStopPattern = /(?:完成了|全部完成|以上就|all done|task complete)/i;
|
|
370
|
+
const tail = text.split('\n').slice(-6).join('\n');
|
|
371
|
+
if (autoStopPattern.test(tail)) return false;
|
|
372
|
+
return autoContinuePattern.test(tail);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── Parse CLI args and run ──
|
|
376
|
+
|
|
377
|
+
async function main(): Promise<void> {
|
|
378
|
+
const args = process.argv.slice(2);
|
|
379
|
+
|
|
380
|
+
// If no args or just "chat", start interactive chat
|
|
381
|
+
if (args.length === 0 || args[0] === 'chat' || (args.length === 1 && !args[0].startsWith('-') && !['task', 'web', 'config', 'init', 'version'].includes(args[0]))) {
|
|
382
|
+
// Check if first arg is an agent name
|
|
383
|
+
const knownAgents = new Set(['fog', 'rain', 'frost', 'snow', 'dew', 'fair']);
|
|
384
|
+
let agent = 'fog';
|
|
385
|
+
let model: string | undefined;
|
|
386
|
+
|
|
387
|
+
for (let i = 0; i < args.length; i++) {
|
|
388
|
+
if (knownAgents.has(args[i])) {
|
|
389
|
+
agent = args[i];
|
|
390
|
+
} else if (args[i] === '-m' || args[i] === '--model') {
|
|
391
|
+
model = args[++i];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
await interactiveChat(agent, model);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
program.parse(process.argv);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
main().catch((e) => {
|
|
403
|
+
console.error(chalk.red(`Fatal error: ${e}`));
|
|
404
|
+
process.exit(1);
|
|
405
|
+
});
|
package/src/cli/mode.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive mode controller — single source of truth for plan/auto/default.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export enum InteractiveMode {
|
|
6
|
+
DEFAULT = 'default',
|
|
7
|
+
PLAN = 'plan',
|
|
8
|
+
AUTO = 'auto',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const CYCLE: InteractiveMode[] = [
|
|
12
|
+
InteractiveMode.DEFAULT,
|
|
13
|
+
InteractiveMode.PLAN,
|
|
14
|
+
InteractiveMode.AUTO,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const LABEL_STYLE: Record<InteractiveMode, [string, string]> = {
|
|
18
|
+
[InteractiveMode.DEFAULT]: ['DEFAULT', 'bold cyan'],
|
|
19
|
+
[InteractiveMode.PLAN]: ['PLAN', 'bold magenta'],
|
|
20
|
+
[InteractiveMode.AUTO]: ['AUTO', 'bold yellow'],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DESCRIPTIONS: Record<InteractiveMode, string> = {
|
|
24
|
+
[InteractiveMode.DEFAULT]: 'smart — router picks the right depth per message',
|
|
25
|
+
[InteractiveMode.PLAN]: 'plan first, confirm before execute',
|
|
26
|
+
[InteractiveMode.AUTO]: 'autonomous reasoning & execution',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class ModeController {
|
|
30
|
+
private _mode: InteractiveMode | null = null;
|
|
31
|
+
|
|
32
|
+
get current(): InteractiveMode {
|
|
33
|
+
if (this._mode === null) {
|
|
34
|
+
this._mode = InteractiveMode.DEFAULT;
|
|
35
|
+
}
|
|
36
|
+
return this._mode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
set(mode: InteractiveMode): InteractiveMode {
|
|
40
|
+
this._mode = mode;
|
|
41
|
+
return mode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
cycle(): InteractiveMode {
|
|
45
|
+
const cur = this.current;
|
|
46
|
+
const idx = CYCLE.indexOf(cur);
|
|
47
|
+
const next = CYCLE[(idx + 1) % CYCLE.length];
|
|
48
|
+
return this.set(next);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
label(): [string, string] {
|
|
52
|
+
return LABEL_STYLE[this.current];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe(): string {
|
|
56
|
+
return DESCRIPTIONS[this.current];
|
|
57
|
+
}
|
|
58
|
+
}
|