skyloom 1.11.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.
- package/.github/workflows/ci.yml +36 -36
- package/README.md +142 -46
- package/config/default.yaml +43 -47
- package/config/models.yaml +155 -0
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/main.js +172 -47
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +9 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent/task.d.ts +58 -0
- package/dist/core/agent/task.d.ts.map +1 -0
- package/dist/core/agent/task.js +83 -0
- package/dist/core/agent/task.js.map +1 -0
- package/dist/core/agent.d.ts +2 -45
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +61 -145
- package/dist/core/agent.js.map +1 -1
- package/dist/core/agent_helpers.d.ts +10 -0
- package/dist/core/agent_helpers.d.ts.map +1 -1
- package/dist/core/agent_helpers.js +39 -0
- package/dist/core/agent_helpers.js.map +1 -1
- package/dist/core/catalog.d.ts +71 -0
- package/dist/core/catalog.d.ts.map +1 -0
- package/dist/core/catalog.js +176 -0
- package/dist/core/catalog.js.map +1 -0
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +12 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/factory.js +16 -16
- package/dist/core/llm.d.ts +7 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +139 -7
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +69 -62
- package/dist/core/memory.js.map +1 -1
- package/dist/core/theme.d.ts +46 -0
- package/dist/core/theme.d.ts.map +1 -0
- package/dist/core/theme.js +42 -0
- package/dist/core/theme.js.map +1 -0
- package/dist/web/server.js +542 -519
- package/dist/web/server.js.map +1 -1
- package/docs/AESTHETIC_DESIGN.md +144 -0
- package/docs/OPTIMIZATION_PLAN.md +178 -0
- package/package.json +60 -60
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/main.ts +425 -316
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +272 -268
- package/src/core/agent/task.ts +100 -0
- package/src/core/agent.ts +1446 -1549
- package/src/core/agent_helpers.ts +496 -461
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -0
- package/src/core/checkpoint.ts +94 -94
- package/src/core/config.ts +20 -4
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +627 -627
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +108 -5
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp_server.ts +176 -176
- package/src/core/memory.ts +1178 -1171
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +342 -342
- package/src/core/theme.ts +65 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +46 -46
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +380 -380
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/web/server.ts +660 -634
- package/src/web/tts.ts +93 -93
- package/tests/agent_helpers.test.ts +48 -0
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -0
- package/tests/config.test.ts +41 -0
- package/tests/icons.test.ts +45 -45
- package/tests/memory.test.ts +147 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/task.test.ts +60 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/vitest.config.ts +17 -17
package/src/core/vector.ts
CHANGED
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 向量语义搜索 — TF-IDF + Cosine similarity, zero dependencies.
|
|
3
|
-
*
|
|
4
|
-
* Replaces n-gram Jaccard as the default semantic scorer.
|
|
5
|
-
* - IDF pre-computed from document corpus
|
|
6
|
-
* - Cosine similarity on TF-IDF vectors
|
|
7
|
-
* - CJK-aware tokenization (bigram for CJK, whitespace for ASCII)
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* const idx = new VectorIndex();
|
|
11
|
-
* idx.addDocuments(docs);
|
|
12
|
-
* const results = idx.search("deploy script", 5);
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/* ═══════════════════════════════════════
|
|
16
|
-
Tokenizer — CJK-aware
|
|
17
|
-
═══════════════════════════════════════ */
|
|
18
|
-
const CJK = /[一-鿿-ゟ가-]/;
|
|
19
|
-
|
|
20
|
-
function tokenize(text: string): string[] {
|
|
21
|
-
const tokens: string[] = [];
|
|
22
|
-
let i = 0;
|
|
23
|
-
while (i < text.length) {
|
|
24
|
-
if (CJK.test(text[i])) {
|
|
25
|
-
if (i + 1 < text.length && CJK.test(text[i + 1])) {
|
|
26
|
-
tokens.push(text.slice(i, i + 2)); i += 2;
|
|
27
|
-
} else {
|
|
28
|
-
tokens.push(text[i]); i++;
|
|
29
|
-
}
|
|
30
|
-
} else if (/[A-Za-z0-9_]/.test(text[i])) {
|
|
31
|
-
let j = i;
|
|
32
|
-
while (j < text.length && /[A-Za-z0-9_]/.test(text[j])) j++;
|
|
33
|
-
tokens.push(text.slice(i, j).toLowerCase()); i = j;
|
|
34
|
-
} else {
|
|
35
|
-
i++;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return tokens;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/* ═══════════════════════════════════════
|
|
42
|
-
TF-IDF Vector computation
|
|
43
|
-
═══════════════════════════════════════ */
|
|
44
|
-
interface DocVector {
|
|
45
|
-
id: string;
|
|
46
|
-
tf: Map<string, number>;
|
|
47
|
-
norm: number;
|
|
48
|
-
content: string;
|
|
49
|
-
meta?: Record<string, any>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export class VectorIndex {
|
|
53
|
-
private docs: DocVector[] = [];
|
|
54
|
-
private idf: Map<string, number> = new Map();
|
|
55
|
-
private totalDocs = 0;
|
|
56
|
-
|
|
57
|
-
/** Add a document to the index. */
|
|
58
|
-
addDocument(id: string, content: string, meta?: Record<string, any>): void {
|
|
59
|
-
const tokens = tokenize(content);
|
|
60
|
-
const tf = new Map<string, number>();
|
|
61
|
-
for (const t of tokens) { tf.set(t, (tf.get(t) || 0) + 1); }
|
|
62
|
-
|
|
63
|
-
// Normalize by doc length
|
|
64
|
-
const tfIdf = new Map<string, number>();
|
|
65
|
-
let normSq = 0;
|
|
66
|
-
for (const [term, freq] of tf) {
|
|
67
|
-
const tfVal = freq / tokens.length;
|
|
68
|
-
const idfVal = this.idf.get(term) || 0;
|
|
69
|
-
const val = tfVal * Math.max(0.1, idfVal);
|
|
70
|
-
tfIdf.set(term, val);
|
|
71
|
-
normSq += val * val;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const norm = Math.sqrt(normSq);
|
|
75
|
-
this.docs.push({ id, tf: tfIdf, norm, content: content.slice(0, 500), meta });
|
|
76
|
-
this.totalDocs++;
|
|
77
|
-
|
|
78
|
-
// Update IDF
|
|
79
|
-
for (const term of tf.keys()) {
|
|
80
|
-
this.idf.set(term, Math.log((this.totalDocs + 1) / ((this.docFrequency(term) + 1))));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
addDocuments(docs: Array<{ id: string; content: string; meta?: Record<string, any> }>): void {
|
|
85
|
-
for (const d of docs) this.addDocument(d.id, d.content, d.meta);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private docFrequency(term: string): number {
|
|
89
|
-
let count = 0;
|
|
90
|
-
for (const d of this.docs) { if (d.tf.has(term)) count++; }
|
|
91
|
-
return count;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Search for documents similar to query. Returns [score, doc] pairs. */
|
|
95
|
-
search(query: string, topK: number = 5, minScore: number = 0.01): Array<[number, DocVector]> {
|
|
96
|
-
const queryTokens = tokenize(query);
|
|
97
|
-
const queryTf = new Map<string, number>();
|
|
98
|
-
for (const t of queryTokens) { queryTf.set(t, (queryTf.get(t) || 0) + 1); }
|
|
99
|
-
|
|
100
|
-
// Query vector
|
|
101
|
-
const qv = new Map<string, number>();
|
|
102
|
-
let qNormSq = 0;
|
|
103
|
-
for (const [term, freq] of queryTf) {
|
|
104
|
-
const tfVal = freq / queryTokens.length;
|
|
105
|
-
const idfVal = this.idf.get(term) || 0;
|
|
106
|
-
const val = tfVal * Math.max(0.1, idfVal);
|
|
107
|
-
qv.set(term, val);
|
|
108
|
-
qNormSq += val * val;
|
|
109
|
-
}
|
|
110
|
-
const qNorm = Math.sqrt(qNormSq);
|
|
111
|
-
if (qNorm === 0) return [];
|
|
112
|
-
|
|
113
|
-
// Cosine similarity against all docs
|
|
114
|
-
const scored: Array<[number, DocVector]> = [];
|
|
115
|
-
for (const doc of this.docs) {
|
|
116
|
-
if (doc.norm === 0) continue;
|
|
117
|
-
let dot = 0;
|
|
118
|
-
for (const [term, qVal] of qv) {
|
|
119
|
-
dot += qVal * (doc.tf.get(term) || 0);
|
|
120
|
-
}
|
|
121
|
-
const score = dot / (qNorm * doc.norm);
|
|
122
|
-
if (score >= minScore) scored.push([score, doc]);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
scored.sort((a, b) => b[0] - a[0]);
|
|
126
|
-
return scored.slice(0, topK);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Remove a document by ID. */
|
|
130
|
-
removeDocument(id: string): void {
|
|
131
|
-
this.docs = this.docs.filter(d => d.id !== id);
|
|
132
|
-
this.totalDocs = this.docs.length;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
get size(): number { return this.docs.length; }
|
|
136
|
-
|
|
137
|
-
clear(): void { this.docs = []; this.idf.clear(); this.totalDocs = 0; }
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/* ═══════════════════════════════════════
|
|
141
|
-
Singleton instance for memory recall
|
|
142
|
-
═══════════════════════════════════════ */
|
|
143
|
-
let globalIndex: VectorIndex | null = null;
|
|
144
|
-
|
|
145
|
-
export function getVectorIndex(): VectorIndex {
|
|
146
|
-
if (!globalIndex) globalIndex = new VectorIndex();
|
|
147
|
-
return globalIndex;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function resetVectorIndex(): void {
|
|
151
|
-
globalIndex = new VectorIndex();
|
|
152
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 向量语义搜索 — TF-IDF + Cosine similarity, zero dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Replaces n-gram Jaccard as the default semantic scorer.
|
|
5
|
+
* - IDF pre-computed from document corpus
|
|
6
|
+
* - Cosine similarity on TF-IDF vectors
|
|
7
|
+
* - CJK-aware tokenization (bigram for CJK, whitespace for ASCII)
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const idx = new VectorIndex();
|
|
11
|
+
* idx.addDocuments(docs);
|
|
12
|
+
* const results = idx.search("deploy script", 5);
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/* ═══════════════════════════════════════
|
|
16
|
+
Tokenizer — CJK-aware
|
|
17
|
+
═══════════════════════════════════════ */
|
|
18
|
+
const CJK = /[一-鿿-ゟ가-]/;
|
|
19
|
+
|
|
20
|
+
function tokenize(text: string): string[] {
|
|
21
|
+
const tokens: string[] = [];
|
|
22
|
+
let i = 0;
|
|
23
|
+
while (i < text.length) {
|
|
24
|
+
if (CJK.test(text[i])) {
|
|
25
|
+
if (i + 1 < text.length && CJK.test(text[i + 1])) {
|
|
26
|
+
tokens.push(text.slice(i, i + 2)); i += 2;
|
|
27
|
+
} else {
|
|
28
|
+
tokens.push(text[i]); i++;
|
|
29
|
+
}
|
|
30
|
+
} else if (/[A-Za-z0-9_]/.test(text[i])) {
|
|
31
|
+
let j = i;
|
|
32
|
+
while (j < text.length && /[A-Za-z0-9_]/.test(text[j])) j++;
|
|
33
|
+
tokens.push(text.slice(i, j).toLowerCase()); i = j;
|
|
34
|
+
} else {
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return tokens;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ═══════════════════════════════════════
|
|
42
|
+
TF-IDF Vector computation
|
|
43
|
+
═══════════════════════════════════════ */
|
|
44
|
+
interface DocVector {
|
|
45
|
+
id: string;
|
|
46
|
+
tf: Map<string, number>;
|
|
47
|
+
norm: number;
|
|
48
|
+
content: string;
|
|
49
|
+
meta?: Record<string, any>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class VectorIndex {
|
|
53
|
+
private docs: DocVector[] = [];
|
|
54
|
+
private idf: Map<string, number> = new Map();
|
|
55
|
+
private totalDocs = 0;
|
|
56
|
+
|
|
57
|
+
/** Add a document to the index. */
|
|
58
|
+
addDocument(id: string, content: string, meta?: Record<string, any>): void {
|
|
59
|
+
const tokens = tokenize(content);
|
|
60
|
+
const tf = new Map<string, number>();
|
|
61
|
+
for (const t of tokens) { tf.set(t, (tf.get(t) || 0) + 1); }
|
|
62
|
+
|
|
63
|
+
// Normalize by doc length
|
|
64
|
+
const tfIdf = new Map<string, number>();
|
|
65
|
+
let normSq = 0;
|
|
66
|
+
for (const [term, freq] of tf) {
|
|
67
|
+
const tfVal = freq / tokens.length;
|
|
68
|
+
const idfVal = this.idf.get(term) || 0;
|
|
69
|
+
const val = tfVal * Math.max(0.1, idfVal);
|
|
70
|
+
tfIdf.set(term, val);
|
|
71
|
+
normSq += val * val;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const norm = Math.sqrt(normSq);
|
|
75
|
+
this.docs.push({ id, tf: tfIdf, norm, content: content.slice(0, 500), meta });
|
|
76
|
+
this.totalDocs++;
|
|
77
|
+
|
|
78
|
+
// Update IDF
|
|
79
|
+
for (const term of tf.keys()) {
|
|
80
|
+
this.idf.set(term, Math.log((this.totalDocs + 1) / ((this.docFrequency(term) + 1))));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
addDocuments(docs: Array<{ id: string; content: string; meta?: Record<string, any> }>): void {
|
|
85
|
+
for (const d of docs) this.addDocument(d.id, d.content, d.meta);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private docFrequency(term: string): number {
|
|
89
|
+
let count = 0;
|
|
90
|
+
for (const d of this.docs) { if (d.tf.has(term)) count++; }
|
|
91
|
+
return count;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Search for documents similar to query. Returns [score, doc] pairs. */
|
|
95
|
+
search(query: string, topK: number = 5, minScore: number = 0.01): Array<[number, DocVector]> {
|
|
96
|
+
const queryTokens = tokenize(query);
|
|
97
|
+
const queryTf = new Map<string, number>();
|
|
98
|
+
for (const t of queryTokens) { queryTf.set(t, (queryTf.get(t) || 0) + 1); }
|
|
99
|
+
|
|
100
|
+
// Query vector
|
|
101
|
+
const qv = new Map<string, number>();
|
|
102
|
+
let qNormSq = 0;
|
|
103
|
+
for (const [term, freq] of queryTf) {
|
|
104
|
+
const tfVal = freq / queryTokens.length;
|
|
105
|
+
const idfVal = this.idf.get(term) || 0;
|
|
106
|
+
const val = tfVal * Math.max(0.1, idfVal);
|
|
107
|
+
qv.set(term, val);
|
|
108
|
+
qNormSq += val * val;
|
|
109
|
+
}
|
|
110
|
+
const qNorm = Math.sqrt(qNormSq);
|
|
111
|
+
if (qNorm === 0) return [];
|
|
112
|
+
|
|
113
|
+
// Cosine similarity against all docs
|
|
114
|
+
const scored: Array<[number, DocVector]> = [];
|
|
115
|
+
for (const doc of this.docs) {
|
|
116
|
+
if (doc.norm === 0) continue;
|
|
117
|
+
let dot = 0;
|
|
118
|
+
for (const [term, qVal] of qv) {
|
|
119
|
+
dot += qVal * (doc.tf.get(term) || 0);
|
|
120
|
+
}
|
|
121
|
+
const score = dot / (qNorm * doc.norm);
|
|
122
|
+
if (score >= minScore) scored.push([score, doc]);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
scored.sort((a, b) => b[0] - a[0]);
|
|
126
|
+
return scored.slice(0, topK);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Remove a document by ID. */
|
|
130
|
+
removeDocument(id: string): void {
|
|
131
|
+
this.docs = this.docs.filter(d => d.id !== id);
|
|
132
|
+
this.totalDocs = this.docs.length;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
get size(): number { return this.docs.length; }
|
|
136
|
+
|
|
137
|
+
clear(): void { this.docs = []; this.idf.clear(); this.totalDocs = 0; }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ═══════════════════════════════════════
|
|
141
|
+
Singleton instance for memory recall
|
|
142
|
+
═══════════════════════════════════════ */
|
|
143
|
+
let globalIndex: VectorIndex | null = null;
|
|
144
|
+
|
|
145
|
+
export function getVectorIndex(): VectorIndex {
|
|
146
|
+
if (!globalIndex) globalIndex = new VectorIndex();
|
|
147
|
+
return globalIndex;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function resetVectorIndex(): void {
|
|
151
|
+
globalIndex = new VectorIndex();
|
|
152
|
+
}
|
package/src/core/workspace.ts
CHANGED
|
@@ -1,150 +1,150 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workspace auto-detection and lifecycle management.
|
|
3
|
-
*
|
|
4
|
-
* Rules:
|
|
5
|
-
* - First launch: auto-select best drive, create workspace/
|
|
6
|
-
* - Multi-drive: skip C:, pick drive with most free space
|
|
7
|
-
* - Single drive (C: only): use C:\\workspace
|
|
8
|
-
* - Unix: use ~/workspace
|
|
9
|
-
* - User can override via config workspace.path
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import * as fs from 'fs';
|
|
13
|
-
import * as os from 'os';
|
|
14
|
-
import * as path from 'path';
|
|
15
|
-
|
|
16
|
-
const WORKSPACE_SUBDIRS = ['files', 'output', 'temp'];
|
|
17
|
-
|
|
18
|
-
export interface DriveInfo {
|
|
19
|
-
letter: string;
|
|
20
|
-
path: string;
|
|
21
|
-
totalBytes: number;
|
|
22
|
-
freeBytes: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Enumerate fixed drives with free-space info.
|
|
27
|
-
*/
|
|
28
|
-
function getDriveList(): DriveInfo[] {
|
|
29
|
-
const drives: DriveInfo[] = [];
|
|
30
|
-
|
|
31
|
-
if (process.platform === 'win32') {
|
|
32
|
-
for (let i = 0; i < 26; i++) {
|
|
33
|
-
const letter = String.fromCharCode(65 + i); // A-Z
|
|
34
|
-
const root = `${letter}:\\`;
|
|
35
|
-
if (!fs.existsSync(root)) continue;
|
|
36
|
-
try {
|
|
37
|
-
const stat = fs.statfsSync(root);
|
|
38
|
-
drives.push({
|
|
39
|
-
letter,
|
|
40
|
-
path: root,
|
|
41
|
-
totalBytes: stat.blocks * stat.bsize,
|
|
42
|
-
freeBytes: stat.bfree * stat.bsize,
|
|
43
|
-
});
|
|
44
|
-
} catch {
|
|
45
|
-
try {
|
|
46
|
-
// Fallback: just mark it available
|
|
47
|
-
drives.push({
|
|
48
|
-
letter,
|
|
49
|
-
path: root,
|
|
50
|
-
totalBytes: 0,
|
|
51
|
-
freeBytes: 0,
|
|
52
|
-
});
|
|
53
|
-
} catch {
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
} else {
|
|
59
|
-
// Unix — treat / as the single candidate
|
|
60
|
-
try {
|
|
61
|
-
const stat = fs.statfsSync('/');
|
|
62
|
-
drives.push({
|
|
63
|
-
letter: '',
|
|
64
|
-
path: '/',
|
|
65
|
-
totalBytes: stat.blocks * stat.bsize,
|
|
66
|
-
freeBytes: stat.bfree * stat.bsize,
|
|
67
|
-
});
|
|
68
|
-
} catch {
|
|
69
|
-
// Ignore
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return drives;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Pick the best drive for the workspace directory.
|
|
78
|
-
*
|
|
79
|
-
* - Windows with multiple drives: skip C:, pick drive with most free bytes.
|
|
80
|
-
* - Windows with only C: → C:\\workspace.
|
|
81
|
-
* - Unix → ~/workspace.
|
|
82
|
-
*/
|
|
83
|
-
export function detectBestWorkspaceRoot(): string {
|
|
84
|
-
const drives = getDriveList();
|
|
85
|
-
|
|
86
|
-
if (process.platform === 'win32') {
|
|
87
|
-
let candidates = drives.filter((d) => d.letter.toUpperCase() !== 'C');
|
|
88
|
-
if (candidates.length === 0) {
|
|
89
|
-
candidates = drives; // fallback to C:
|
|
90
|
-
}
|
|
91
|
-
// Sort by free space descending
|
|
92
|
-
candidates.sort((a, b) => b.freeBytes - a.freeBytes);
|
|
93
|
-
const best = candidates[0];
|
|
94
|
-
return path.join(best.path, 'workspace');
|
|
95
|
-
} else {
|
|
96
|
-
return path.join(os.homedir(), 'workspace');
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Resolve workspace path from config.
|
|
102
|
-
*
|
|
103
|
-
* - "auto" → call detectBestWorkspaceRoot()
|
|
104
|
-
* - explicit path → expand ~ and return
|
|
105
|
-
*/
|
|
106
|
-
export function resolveWorkspacePath(configValue: string): string {
|
|
107
|
-
if (configValue.toLowerCase() === 'auto') {
|
|
108
|
-
return detectBestWorkspaceRoot();
|
|
109
|
-
}
|
|
110
|
-
return path.resolve(configValue.replace(/^~/, os.homedir()));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Create workspace directory tree on first use. Idempotent.
|
|
115
|
-
*
|
|
116
|
-
* Creates:
|
|
117
|
-
* workspace/
|
|
118
|
-
* ├── files/ # agent-generated files
|
|
119
|
-
* ├── output/ # task results, exports
|
|
120
|
-
* └── temp/ # scratch / ephemeral
|
|
121
|
-
*
|
|
122
|
-
* Returns the resolved workspace root path.
|
|
123
|
-
*/
|
|
124
|
-
export function initWorkspace(root: string): string {
|
|
125
|
-
fs.mkdirSync(root, { recursive: true });
|
|
126
|
-
for (const sub of WORKSPACE_SUBDIRS) {
|
|
127
|
-
fs.mkdirSync(path.join(root, sub), { recursive: true });
|
|
128
|
-
}
|
|
129
|
-
// Touch a .workspace marker so tools can identify it
|
|
130
|
-
const marker = path.join(root, '.workspace');
|
|
131
|
-
if (!fs.existsSync(marker)) {
|
|
132
|
-
fs.writeFileSync(marker, `# Skyloom workspace — created automatically\npath: ${root}\n`, 'utf-8');
|
|
133
|
-
}
|
|
134
|
-
return root;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Human-readable byte count (e.g. 128.5 GB).
|
|
139
|
-
*/
|
|
140
|
-
export function formatBytes(n: number): string {
|
|
141
|
-
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
142
|
-
let val = n;
|
|
143
|
-
for (const unit of units) {
|
|
144
|
-
if (Math.abs(val) < 1024.0) {
|
|
145
|
-
return `${val.toFixed(1)} ${unit}`;
|
|
146
|
-
}
|
|
147
|
-
val /= 1024.0;
|
|
148
|
-
}
|
|
149
|
-
return `${val.toFixed(1)} PB`;
|
|
150
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Workspace auto-detection and lifecycle management.
|
|
3
|
+
*
|
|
4
|
+
* Rules:
|
|
5
|
+
* - First launch: auto-select best drive, create workspace/
|
|
6
|
+
* - Multi-drive: skip C:, pick drive with most free space
|
|
7
|
+
* - Single drive (C: only): use C:\\workspace
|
|
8
|
+
* - Unix: use ~/workspace
|
|
9
|
+
* - User can override via config workspace.path
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
|
|
16
|
+
const WORKSPACE_SUBDIRS = ['files', 'output', 'temp'];
|
|
17
|
+
|
|
18
|
+
export interface DriveInfo {
|
|
19
|
+
letter: string;
|
|
20
|
+
path: string;
|
|
21
|
+
totalBytes: number;
|
|
22
|
+
freeBytes: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Enumerate fixed drives with free-space info.
|
|
27
|
+
*/
|
|
28
|
+
function getDriveList(): DriveInfo[] {
|
|
29
|
+
const drives: DriveInfo[] = [];
|
|
30
|
+
|
|
31
|
+
if (process.platform === 'win32') {
|
|
32
|
+
for (let i = 0; i < 26; i++) {
|
|
33
|
+
const letter = String.fromCharCode(65 + i); // A-Z
|
|
34
|
+
const root = `${letter}:\\`;
|
|
35
|
+
if (!fs.existsSync(root)) continue;
|
|
36
|
+
try {
|
|
37
|
+
const stat = fs.statfsSync(root);
|
|
38
|
+
drives.push({
|
|
39
|
+
letter,
|
|
40
|
+
path: root,
|
|
41
|
+
totalBytes: stat.blocks * stat.bsize,
|
|
42
|
+
freeBytes: stat.bfree * stat.bsize,
|
|
43
|
+
});
|
|
44
|
+
} catch {
|
|
45
|
+
try {
|
|
46
|
+
// Fallback: just mark it available
|
|
47
|
+
drives.push({
|
|
48
|
+
letter,
|
|
49
|
+
path: root,
|
|
50
|
+
totalBytes: 0,
|
|
51
|
+
freeBytes: 0,
|
|
52
|
+
});
|
|
53
|
+
} catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// Unix — treat / as the single candidate
|
|
60
|
+
try {
|
|
61
|
+
const stat = fs.statfsSync('/');
|
|
62
|
+
drives.push({
|
|
63
|
+
letter: '',
|
|
64
|
+
path: '/',
|
|
65
|
+
totalBytes: stat.blocks * stat.bsize,
|
|
66
|
+
freeBytes: stat.bfree * stat.bsize,
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
// Ignore
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return drives;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Pick the best drive for the workspace directory.
|
|
78
|
+
*
|
|
79
|
+
* - Windows with multiple drives: skip C:, pick drive with most free bytes.
|
|
80
|
+
* - Windows with only C: → C:\\workspace.
|
|
81
|
+
* - Unix → ~/workspace.
|
|
82
|
+
*/
|
|
83
|
+
export function detectBestWorkspaceRoot(): string {
|
|
84
|
+
const drives = getDriveList();
|
|
85
|
+
|
|
86
|
+
if (process.platform === 'win32') {
|
|
87
|
+
let candidates = drives.filter((d) => d.letter.toUpperCase() !== 'C');
|
|
88
|
+
if (candidates.length === 0) {
|
|
89
|
+
candidates = drives; // fallback to C:
|
|
90
|
+
}
|
|
91
|
+
// Sort by free space descending
|
|
92
|
+
candidates.sort((a, b) => b.freeBytes - a.freeBytes);
|
|
93
|
+
const best = candidates[0];
|
|
94
|
+
return path.join(best.path, 'workspace');
|
|
95
|
+
} else {
|
|
96
|
+
return path.join(os.homedir(), 'workspace');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve workspace path from config.
|
|
102
|
+
*
|
|
103
|
+
* - "auto" → call detectBestWorkspaceRoot()
|
|
104
|
+
* - explicit path → expand ~ and return
|
|
105
|
+
*/
|
|
106
|
+
export function resolveWorkspacePath(configValue: string): string {
|
|
107
|
+
if (configValue.toLowerCase() === 'auto') {
|
|
108
|
+
return detectBestWorkspaceRoot();
|
|
109
|
+
}
|
|
110
|
+
return path.resolve(configValue.replace(/^~/, os.homedir()));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create workspace directory tree on first use. Idempotent.
|
|
115
|
+
*
|
|
116
|
+
* Creates:
|
|
117
|
+
* workspace/
|
|
118
|
+
* ├── files/ # agent-generated files
|
|
119
|
+
* ├── output/ # task results, exports
|
|
120
|
+
* └── temp/ # scratch / ephemeral
|
|
121
|
+
*
|
|
122
|
+
* Returns the resolved workspace root path.
|
|
123
|
+
*/
|
|
124
|
+
export function initWorkspace(root: string): string {
|
|
125
|
+
fs.mkdirSync(root, { recursive: true });
|
|
126
|
+
for (const sub of WORKSPACE_SUBDIRS) {
|
|
127
|
+
fs.mkdirSync(path.join(root, sub), { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
// Touch a .workspace marker so tools can identify it
|
|
130
|
+
const marker = path.join(root, '.workspace');
|
|
131
|
+
if (!fs.existsSync(marker)) {
|
|
132
|
+
fs.writeFileSync(marker, `# Skyloom workspace — created automatically\npath: ${root}\n`, 'utf-8');
|
|
133
|
+
}
|
|
134
|
+
return root;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Human-readable byte count (e.g. 128.5 GB).
|
|
139
|
+
*/
|
|
140
|
+
export function formatBytes(n: number): string {
|
|
141
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
142
|
+
let val = n;
|
|
143
|
+
for (const unit of units) {
|
|
144
|
+
if (Math.abs(val) < 1024.0) {
|
|
145
|
+
return `${val.toFixed(1)} ${unit}`;
|
|
146
|
+
}
|
|
147
|
+
val /= 1024.0;
|
|
148
|
+
}
|
|
149
|
+
return `${val.toFixed(1)} PB`;
|
|
150
|
+
}
|