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.
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 -0
  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 +172 -47
  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 +9 -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 -316
  83. package/src/cli/mode.ts +58 -58
  84. package/src/cli/tui.ts +272 -268
  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,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
+ }
@@ -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
+ }