nova-terminal-assistant 0.1.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.

Potentially problematic release.


This version of nova-terminal-assistant might be problematic. Click here for more details.

Files changed (192) hide show
  1. package/README.md +358 -0
  2. package/bin/nova +38 -0
  3. package/bin/nova.js +12 -0
  4. package/package.json +67 -0
  5. package/src/cli/commands/SmartCompletion.ts +458 -0
  6. package/src/cli/index.ts +5 -0
  7. package/src/cli/startup/IFlowRepl.ts +212 -0
  8. package/src/cli/startup/InkBasedRepl.ts +1056 -0
  9. package/src/cli/startup/InteractiveRepl.ts +2833 -0
  10. package/src/cli/startup/NovaApp.ts +1861 -0
  11. package/src/cli/startup/index.ts +4 -0
  12. package/src/cli/startup/parseArgs.ts +293 -0
  13. package/src/cli/test-modules.ts +27 -0
  14. package/src/cli/ui/IFlowDropdown.ts +425 -0
  15. package/src/cli/ui/ModernReplUI.ts +276 -0
  16. package/src/cli/ui/SimpleSelector2.ts +215 -0
  17. package/src/cli/ui/components/ConfirmDialog.ts +176 -0
  18. package/src/cli/ui/components/ErrorPanel.ts +364 -0
  19. package/src/cli/ui/components/InkAppRunner.tsx +67 -0
  20. package/src/cli/ui/components/InkComponents.tsx +613 -0
  21. package/src/cli/ui/components/NovaInkApp.tsx +312 -0
  22. package/src/cli/ui/components/ProgressBar.ts +177 -0
  23. package/src/cli/ui/components/ProgressIndicator.ts +298 -0
  24. package/src/cli/ui/components/QuickActions.ts +396 -0
  25. package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
  26. package/src/cli/ui/components/StatusBar.ts +194 -0
  27. package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
  28. package/src/cli/ui/components/index.ts +27 -0
  29. package/src/cli/ui/ink-prototype.tsx +347 -0
  30. package/src/cli/utils/CliUI.ts +336 -0
  31. package/src/cli/utils/CompletionHelper.ts +388 -0
  32. package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
  33. package/src/cli/utils/EnhancedCompleter.ts +513 -0
  34. package/src/cli/utils/ErrorEnhancer.ts +429 -0
  35. package/src/cli/utils/OutputFormatter.ts +193 -0
  36. package/src/cli/utils/index.ts +9 -0
  37. package/src/core/agents/AgentOrchestrator.ts +515 -0
  38. package/src/core/agents/index.ts +17 -0
  39. package/src/core/audit/AuditLogger.ts +509 -0
  40. package/src/core/audit/index.ts +11 -0
  41. package/src/core/auth/AuthManager.d.ts.map +1 -0
  42. package/src/core/auth/AuthManager.ts +138 -0
  43. package/src/core/auth/index.d.ts.map +1 -0
  44. package/src/core/auth/index.ts +2 -0
  45. package/src/core/config/ConfigManager.d.ts.map +1 -0
  46. package/src/core/config/ConfigManager.test.ts +183 -0
  47. package/src/core/config/ConfigManager.ts +1219 -0
  48. package/src/core/config/index.d.ts.map +1 -0
  49. package/src/core/config/index.ts +1 -0
  50. package/src/core/context/ContextBuilder.d.ts.map +1 -0
  51. package/src/core/context/ContextBuilder.ts +171 -0
  52. package/src/core/context/ContextCompressor.d.ts.map +1 -0
  53. package/src/core/context/ContextCompressor.ts +642 -0
  54. package/src/core/context/LayeredMemoryManager.ts +657 -0
  55. package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
  56. package/src/core/context/MemoryDiscovery.ts +175 -0
  57. package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
  58. package/src/core/context/defaultSystemPrompt.ts +35 -0
  59. package/src/core/context/index.d.ts.map +1 -0
  60. package/src/core/context/index.ts +22 -0
  61. package/src/core/extensions/SkillGenerator.ts +421 -0
  62. package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
  63. package/src/core/extensions/SkillInstaller.ts +257 -0
  64. package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
  65. package/src/core/extensions/SkillRegistry.ts +361 -0
  66. package/src/core/extensions/SkillValidator.ts +525 -0
  67. package/src/core/extensions/index.ts +15 -0
  68. package/src/core/index.d.ts.map +1 -0
  69. package/src/core/index.ts +42 -0
  70. package/src/core/mcp/McpManager.d.ts.map +1 -0
  71. package/src/core/mcp/McpManager.ts +632 -0
  72. package/src/core/mcp/index.d.ts.map +1 -0
  73. package/src/core/mcp/index.ts +2 -0
  74. package/src/core/model/ModelClient.d.ts.map +1 -0
  75. package/src/core/model/ModelClient.ts +217 -0
  76. package/src/core/model/ModelConnectionTester.ts +363 -0
  77. package/src/core/model/ModelValidator.ts +348 -0
  78. package/src/core/model/index.d.ts.map +1 -0
  79. package/src/core/model/index.ts +6 -0
  80. package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
  81. package/src/core/model/providers/AnthropicProvider.ts +279 -0
  82. package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
  83. package/src/core/model/providers/CodingPlanProvider.ts +210 -0
  84. package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
  85. package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
  86. package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
  87. package/src/core/model/providers/OllamaManager.ts +201 -0
  88. package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
  89. package/src/core/model/providers/OllamaProvider.ts +73 -0
  90. package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  91. package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
  92. package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
  93. package/src/core/model/providers/OpenAIProvider.ts +29 -0
  94. package/src/core/model/providers/index.d.ts.map +1 -0
  95. package/src/core/model/providers/index.ts +12 -0
  96. package/src/core/model/types.d.ts.map +1 -0
  97. package/src/core/model/types.ts +77 -0
  98. package/src/core/security/ApprovalManager.d.ts.map +1 -0
  99. package/src/core/security/ApprovalManager.ts +174 -0
  100. package/src/core/security/FileFilter.d.ts.map +1 -0
  101. package/src/core/security/FileFilter.ts +141 -0
  102. package/src/core/security/HookExecutor.d.ts.map +1 -0
  103. package/src/core/security/HookExecutor.ts +178 -0
  104. package/src/core/security/SandboxExecutor.ts +447 -0
  105. package/src/core/security/index.d.ts.map +1 -0
  106. package/src/core/security/index.ts +8 -0
  107. package/src/core/session/AgentLoop.d.ts.map +1 -0
  108. package/src/core/session/AgentLoop.ts +501 -0
  109. package/src/core/session/SessionManager.d.ts.map +1 -0
  110. package/src/core/session/SessionManager.test.ts +183 -0
  111. package/src/core/session/SessionManager.ts +460 -0
  112. package/src/core/session/index.d.ts.map +1 -0
  113. package/src/core/session/index.ts +3 -0
  114. package/src/core/telemetry/Telemetry.d.ts.map +1 -0
  115. package/src/core/telemetry/Telemetry.ts +90 -0
  116. package/src/core/telemetry/TelemetryService.ts +531 -0
  117. package/src/core/telemetry/index.d.ts.map +1 -0
  118. package/src/core/telemetry/index.ts +12 -0
  119. package/src/core/testing/AutoFixer.ts +385 -0
  120. package/src/core/testing/ErrorAnalyzer.ts +499 -0
  121. package/src/core/testing/TestRunner.ts +265 -0
  122. package/src/core/testing/agent-cli-tests.ts +538 -0
  123. package/src/core/testing/index.ts +11 -0
  124. package/src/core/tools/ToolRegistry.d.ts.map +1 -0
  125. package/src/core/tools/ToolRegistry.test.ts +206 -0
  126. package/src/core/tools/ToolRegistry.ts +260 -0
  127. package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
  128. package/src/core/tools/impl/EditFileTool.ts +97 -0
  129. package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
  130. package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
  131. package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
  132. package/src/core/tools/impl/MemoryTool.ts +102 -0
  133. package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
  134. package/src/core/tools/impl/ReadFileTool.ts +58 -0
  135. package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
  136. package/src/core/tools/impl/SearchContentTool.ts +94 -0
  137. package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
  138. package/src/core/tools/impl/SearchFileTool.ts +61 -0
  139. package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
  140. package/src/core/tools/impl/ShellTool.ts +118 -0
  141. package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
  142. package/src/core/tools/impl/TaskTool.ts +207 -0
  143. package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
  144. package/src/core/tools/impl/TodoTool.ts +122 -0
  145. package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
  146. package/src/core/tools/impl/WebFetchTool.ts +103 -0
  147. package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
  148. package/src/core/tools/impl/WebSearchTool.ts +89 -0
  149. package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
  150. package/src/core/tools/impl/WriteFileTool.ts +49 -0
  151. package/src/core/tools/impl/index.d.ts.map +1 -0
  152. package/src/core/tools/impl/index.ts +16 -0
  153. package/src/core/tools/index.d.ts.map +1 -0
  154. package/src/core/tools/index.ts +7 -0
  155. package/src/core/tools/schemas/execution.d.ts.map +1 -0
  156. package/src/core/tools/schemas/execution.ts +42 -0
  157. package/src/core/tools/schemas/file.d.ts.map +1 -0
  158. package/src/core/tools/schemas/file.ts +119 -0
  159. package/src/core/tools/schemas/index.d.ts.map +1 -0
  160. package/src/core/tools/schemas/index.ts +11 -0
  161. package/src/core/tools/schemas/memory.d.ts.map +1 -0
  162. package/src/core/tools/schemas/memory.ts +52 -0
  163. package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
  164. package/src/core/tools/schemas/orchestration.ts +44 -0
  165. package/src/core/tools/schemas/search.d.ts.map +1 -0
  166. package/src/core/tools/schemas/search.ts +112 -0
  167. package/src/core/tools/schemas/todo.d.ts.map +1 -0
  168. package/src/core/tools/schemas/todo.ts +32 -0
  169. package/src/core/tools/schemas/web.d.ts.map +1 -0
  170. package/src/core/tools/schemas/web.ts +86 -0
  171. package/src/core/types/config.d.ts.map +1 -0
  172. package/src/core/types/config.ts +200 -0
  173. package/src/core/types/errors.d.ts.map +1 -0
  174. package/src/core/types/errors.ts +204 -0
  175. package/src/core/types/index.d.ts.map +1 -0
  176. package/src/core/types/index.ts +8 -0
  177. package/src/core/types/session.d.ts.map +1 -0
  178. package/src/core/types/session.ts +216 -0
  179. package/src/core/types/tools.d.ts.map +1 -0
  180. package/src/core/types/tools.ts +157 -0
  181. package/src/core/utils/CheckpointManager.d.ts.map +1 -0
  182. package/src/core/utils/CheckpointManager.ts +327 -0
  183. package/src/core/utils/Logger.d.ts.map +1 -0
  184. package/src/core/utils/Logger.ts +98 -0
  185. package/src/core/utils/RetryManager.ts +471 -0
  186. package/src/core/utils/TokenCounter.d.ts.map +1 -0
  187. package/src/core/utils/TokenCounter.ts +414 -0
  188. package/src/core/utils/VectorMemoryStore.ts +440 -0
  189. package/src/core/utils/helpers.d.ts.map +1 -0
  190. package/src/core/utils/helpers.ts +89 -0
  191. package/src/core/utils/index.d.ts.map +1 -0
  192. package/src/core/utils/index.ts +19 -0
@@ -0,0 +1,257 @@
1
+ // ============================================================================
2
+ // SkillInstaller - Install skills from GitHub repositories
3
+ // ============================================================================
4
+
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ import { execSync } from 'node:child_process';
9
+
10
+ export interface SkillInstallOptions {
11
+ /** GitHub repository URL or shorthand (e.g., "obra/superpowers") */
12
+ source: string;
13
+ /** Target directory for installation (default: ~/.nova/skills/) */
14
+ targetDir?: string;
15
+ /** Specific skill names to install (default: all) */
16
+ skills?: string[];
17
+ /** Force overwrite existing skills */
18
+ force?: boolean;
19
+ }
20
+
21
+ export interface InstalledSkill {
22
+ name: string;
23
+ path: string;
24
+ source: string;
25
+ }
26
+
27
+ /**
28
+ * Install skills from GitHub repositories
29
+ *
30
+ * Supports formats:
31
+ * - "obra/superpowers" -> https://github.com/obra/superpowers
32
+ * - "https://github.com/obra/superpowers"
33
+ * - Full GitHub URL
34
+ */
35
+ export class SkillInstaller {
36
+ private baseDir: string;
37
+
38
+ constructor(baseDir?: string) {
39
+ this.baseDir = baseDir || path.join(os.homedir(), '.nova', 'skills');
40
+ }
41
+
42
+ /**
43
+ * Install skills from a GitHub repository
44
+ */
45
+ async install(options: SkillInstallOptions): Promise<InstalledSkill[]> {
46
+ const { source, targetDir, skills: specificSkills, force } = options;
47
+ const target = targetDir || this.baseDir;
48
+
49
+ // Parse GitHub URL
50
+ const repoUrl = this.parseGitHubUrl(source);
51
+ const repoName = repoUrl.split('/').pop() || 'unknown';
52
+
53
+ console.log(`Installing skills from ${repoUrl}...`);
54
+
55
+ // Create temp directory for cloning
56
+ const tempDir = path.join(os.tmpdir(), `nova-skills-${Date.now()}`);
57
+
58
+ try {
59
+ // Clone repository
60
+ console.log(` Cloning repository...`);
61
+ execSync(`git clone --depth 1 ${repoUrl} "${tempDir}"`, { stdio: 'pipe' });
62
+
63
+ // Find skills directory
64
+ const skillsDir = this.findSkillsDirectory(tempDir);
65
+ if (!skillsDir) {
66
+ throw new Error(`No skills directory found in ${repoName}`);
67
+ }
68
+
69
+ // Get all skills
70
+ const availableSkills = this.listSkills(skillsDir);
71
+ const toInstall = specificSkills
72
+ ? availableSkills.filter(s => specificSkills.includes(s))
73
+ : availableSkills;
74
+
75
+ if (toInstall.length === 0) {
76
+ throw new Error(`No matching skills found`);
77
+ }
78
+
79
+ // Install each skill
80
+ const installed: InstalledSkill[] = [];
81
+ for (const skillName of toInstall) {
82
+ const srcPath = path.join(skillsDir, skillName);
83
+ const destPath = path.join(target, skillName);
84
+
85
+ // Check if exists
86
+ if (fs.existsSync(destPath) && !force) {
87
+ console.log(` ⚠ ${skillName} already exists, use --force to overwrite`);
88
+ continue;
89
+ }
90
+
91
+ // Copy skill
92
+ this.copySkill(srcPath, destPath);
93
+ installed.push({ name: skillName, path: destPath, source: repoUrl });
94
+ console.log(` ✓ Installed: ${skillName}`);
95
+ }
96
+
97
+ return installed;
98
+ } finally {
99
+ // Cleanup temp directory
100
+ this.rmrf(tempDir);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Parse various Git repository URL formats (GitHub, Gitee, etc.)
106
+ */
107
+ private parseGitHubUrl(source: string): string {
108
+ // Already a full URL (GitHub, Gitee, GitLab, etc.)
109
+ if (source.startsWith('https://') || source.startsWith('git@')) {
110
+ return source;
111
+ }
112
+
113
+ // Gitee shorthand: "gitee:owner/repo"
114
+ if (source.startsWith('gitee:')) {
115
+ const repoPath = source.substring(6); // Remove 'gitee:'
116
+ return `https://gitee.com/${repoPath}.git`;
117
+ }
118
+
119
+ // GitHub shorthand: "owner/repo"
120
+ if (source.match(/^[\w-]+\/[\w-]+$/)) {
121
+ return `https://github.com/${source}.git`;
122
+ }
123
+
124
+ // Assume it's a GitHub shorthand
125
+ return `https://github.com/${source}.git`;
126
+ }
127
+
128
+ /**
129
+ * Find the skills directory in a repository
130
+ */
131
+ private findSkillsDirectory(repoDir: string): string | null {
132
+ // Common skill directory names
133
+ const candidates = ['skills', '.claude/skills', 'plugins', '.skills'];
134
+
135
+ for (const name of candidates) {
136
+ const fullPath = path.join(repoDir, name);
137
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
138
+ // Verify it contains SKILL.md files
139
+ const entries = fs.readdirSync(fullPath);
140
+ const hasSkills = entries.some(entry => {
141
+ const skillPath = path.join(fullPath, entry);
142
+ return fs.statSync(skillPath).isDirectory() &&
143
+ fs.existsSync(path.join(skillPath, 'SKILL.md'));
144
+ });
145
+ if (hasSkills) return fullPath;
146
+ }
147
+ }
148
+
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * List all skill directories
154
+ */
155
+ private listSkills(skillsDir: string): string[] {
156
+ return fs.readdirSync(skillsDir).filter(entry => {
157
+ const skillPath = path.join(skillsDir, entry);
158
+ return fs.statSync(skillPath).isDirectory() &&
159
+ fs.existsSync(path.join(skillPath, 'SKILL.md'));
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Copy a skill directory
165
+ */
166
+ private copySkill(src: string, dest: string): void {
167
+ // Create destination
168
+ fs.mkdirSync(dest, { recursive: true });
169
+
170
+ // Copy all files
171
+ const entries = fs.readdirSync(src, { withFileTypes: true });
172
+ for (const entry of entries) {
173
+ const srcPath = path.join(src, entry.name);
174
+ const destPath = path.join(dest, entry.name);
175
+
176
+ if (entry.isDirectory()) {
177
+ this.copySkill(srcPath, destPath);
178
+ } else {
179
+ fs.copyFileSync(srcPath, destPath);
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Recursive delete
186
+ */
187
+ private rmrf(dir: string): void {
188
+ if (!fs.existsSync(dir)) return;
189
+
190
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
191
+ for (const entry of entries) {
192
+ const fullPath = path.join(dir, entry.name);
193
+ if (entry.isDirectory()) {
194
+ this.rmrf(fullPath);
195
+ } else {
196
+ fs.unlinkSync(fullPath);
197
+ }
198
+ }
199
+ fs.rmdirSync(dir);
200
+ }
201
+
202
+ /**
203
+ * List installed skills
204
+ */
205
+ listInstalled(): string[] {
206
+ if (!fs.existsSync(this.baseDir)) return [];
207
+ return fs.readdirSync(this.baseDir).filter(name => {
208
+ return fs.existsSync(path.join(this.baseDir, name, 'SKILL.md'));
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Uninstall a skill
214
+ */
215
+ uninstall(skillName: string): boolean {
216
+ const skillPath = path.join(this.baseDir, skillName);
217
+ if (!fs.existsSync(skillPath)) return false;
218
+ this.rmrf(skillPath);
219
+ return true;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Popular skill repositories
225
+ */
226
+ export const POPULAR_SKILL_REPOS = {
227
+ superpowers: {
228
+ // Default to Gitee mirror for better accessibility in China
229
+ url: 'gitee:anderson2/superpowers',
230
+ description: 'Agentic skills framework - TDD, debugging, code review, planning',
231
+ skills: [
232
+ 'brainstorming',
233
+ 'writing-plans',
234
+ 'executing-plans',
235
+ 'test-driven-development',
236
+ 'systematic-debugging',
237
+ 'requesting-code-review',
238
+ 'receiving-code-review',
239
+ 'using-git-worktrees',
240
+ 'finishing-a-development-branch',
241
+ 'subagent-driven-development',
242
+ 'verification-before-completion',
243
+ 'writing-skills',
244
+ ],
245
+ },
246
+ };
247
+
248
+ /**
249
+ * Quick install function
250
+ */
251
+ export async function installSuperpowers(baseDir?: string): Promise<InstalledSkill[]> {
252
+ const installer = new SkillInstaller(baseDir);
253
+ return installer.install({
254
+ source: 'gitee:anderson2/superpowers',
255
+ force: false,
256
+ });
257
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SkillRegistry.d.ts","sourceRoot":"","sources":["SkillRegistry.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,oCAAoC;IACpC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,4CAA4C;IAC5C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,oBAAoB;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,uBAAuB;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uBAAuB;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,qBAAqB;IACrB,QAAQ,EAAE,aAAa,CAAC;IACxB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,0BAA0B;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID,qBAAa,aAAa;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,KAAK,CAAsC;IACnD,OAAO,CAAC,WAAW,CAAS;gBAEhB,SAAS,CAAC,EAAE,MAAM;IAI9B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBjC;;OAEG;IACG,QAAQ,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBrD;;OAEG;IACG,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAYxD;;OAEG;IACG,MAAM,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IA4DnE;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAOxC;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAa5C;;OAEG;IACH,YAAY,IAAI,MAAM;IAItB;;OAEG;IACH,OAAO,CAAC,YAAY;IAkBpB;;OAEG;YACW,SAAS;IAkCvB;;OAEG;IACH,OAAO,CAAC,YAAY;IAwDpB;;OAEG;YACW,eAAe;CAG9B"}
@@ -0,0 +1,361 @@
1
+ // ============================================================================
2
+ // SkillRegistry - Register, discover, and manage agent skills
3
+ // Reference: WorkBuddy skill system with metadata tracking
4
+ // ============================================================================
5
+
6
+ import { readFile, writeFile, mkdir, readdir, stat, rm } from 'node:fs/promises';
7
+ import { join, resolve, dirname } from 'node:path';
8
+
9
+ // --- Types ---
10
+
11
+ export interface SkillMetadata {
12
+ /** Unique skill identifier */
13
+ name: string;
14
+ /** Human-readable description */
15
+ description: string;
16
+ /** Author */
17
+ author?: string;
18
+ /** Version (semver) */
19
+ version: string;
20
+ /** Tags for categorization and search */
21
+ tags: string[];
22
+ /** Supported model providers (empty = all) */
23
+ providers?: string[];
24
+ /** Required tools for this skill */
25
+ requiredTools?: string[];
26
+ /** Whether this skill was auto-generated */
27
+ autoGenerated?: boolean;
28
+ /** Creation date */
29
+ createdAt: string;
30
+ /** Last update date */
31
+ updatedAt: string;
32
+ /** File size in bytes */
33
+ sizeBytes?: number;
34
+ /** Reviewed at date */
35
+ reviewedAt?: string;
36
+ }
37
+
38
+ export interface SkillDefinition {
39
+ /** Skill metadata */
40
+ metadata: SkillMetadata;
41
+ /** SKILL.md content (the skill prompt/instructions) */
42
+ content: string;
43
+ /** Associated script files */
44
+ scripts?: Record<string, string>;
45
+ /** Associated reference files */
46
+ references?: string[];
47
+ }
48
+
49
+ export interface SkillSearchParams {
50
+ /** Search query string */
51
+ query?: string;
52
+ /** Filter by tags */
53
+ tags?: string[];
54
+ /** Filter by provider compatibility */
55
+ provider?: string;
56
+ /** Include auto-generated skills */
57
+ includeAutoGenerated?: boolean;
58
+ /** Maximum results */
59
+ limit?: number;
60
+ }
61
+
62
+ // --- SkillRegistry ---
63
+
64
+ export class SkillRegistry {
65
+ private skillsDir: string;
66
+ private cache = new Map<string, SkillDefinition>();
67
+ private initialized = false;
68
+
69
+ constructor(skillsDir?: string) {
70
+ this.skillsDir = skillsDir || join(process.env.HOME || process.env.USERPROFILE || '~', '.nova-cli', 'skills');
71
+ }
72
+
73
+ /**
74
+ * Initialize the registry by loading all skills from disk.
75
+ */
76
+ async initialize(): Promise<void> {
77
+ try {
78
+ await mkdir(this.skillsDir, { recursive: true });
79
+ } catch {
80
+ // Directory may already exist
81
+ }
82
+
83
+ const entries = await readdir(this.skillsDir, { withFileTypes: true });
84
+ for (const entry of entries) {
85
+ if (entry.isDirectory()) {
86
+ const skillPath = join(this.skillsDir, entry.name);
87
+ try {
88
+ const skill = await this.loadSkill(entry.name);
89
+ if (skill) {
90
+ this.cache.set(entry.name, skill);
91
+ }
92
+ } catch {
93
+ // Skip invalid skills
94
+ }
95
+ }
96
+ }
97
+
98
+ this.initialized = true;
99
+ }
100
+
101
+ /**
102
+ * Register a new skill.
103
+ */
104
+ async register(skill: SkillDefinition): Promise<void> {
105
+ const skillDir = join(this.skillsDir, skill.metadata.name);
106
+ await mkdir(skillDir, { recursive: true });
107
+
108
+ // Write SKILL.md
109
+ const skillMdPath = join(skillDir, 'SKILL.md');
110
+ const fullContent = this.buildSkillMd(skill.metadata, skill.content);
111
+ await writeFile(skillMdPath, fullContent, 'utf-8');
112
+
113
+ // Write script files
114
+ if (skill.scripts) {
115
+ const scriptsDir = join(skillDir, 'scripts');
116
+ await mkdir(scriptsDir, { recursive: true });
117
+ for (const [filename, content] of Object.entries(skill.scripts)) {
118
+ await writeFile(join(scriptsDir, filename), content, 'utf-8');
119
+ }
120
+ }
121
+
122
+ // Update cache
123
+ skill.metadata.updatedAt = new Date().toISOString();
124
+ this.cache.set(skill.metadata.name, skill);
125
+ }
126
+
127
+ /**
128
+ * Get a skill by name.
129
+ */
130
+ async get(name: string): Promise<SkillDefinition | null> {
131
+ if (!this.initialized) {
132
+ await this.initialize();
133
+ }
134
+
135
+ if (this.cache.has(name)) {
136
+ return this.cache.get(name)!;
137
+ }
138
+
139
+ return this.loadSkill(name);
140
+ }
141
+
142
+ /**
143
+ * Search for skills matching the given criteria.
144
+ */
145
+ async search(params: SkillSearchParams): Promise<SkillDefinition[]> {
146
+ if (!this.initialized) {
147
+ await this.initialize();
148
+ }
149
+
150
+ let results = Array.from(this.cache.values());
151
+
152
+ // Filter by auto-generated
153
+ if (!params.includeAutoGenerated) {
154
+ results = results.filter((s) => !s.metadata.autoGenerated);
155
+ }
156
+
157
+ // Filter by provider
158
+ if (params.provider) {
159
+ results = results.filter(
160
+ (s) =>
161
+ !s.metadata.providers ||
162
+ s.metadata.providers.length === 0 ||
163
+ s.metadata.providers.includes(params.provider!)
164
+ );
165
+ }
166
+
167
+ // Filter by tags
168
+ if (params.tags && params.tags.length > 0) {
169
+ results = results.filter((s) =>
170
+ params.tags!.some((tag) => s.metadata.tags.includes(tag))
171
+ );
172
+ }
173
+
174
+ // Filter by query
175
+ if (params.query) {
176
+ const query = params.query.toLowerCase();
177
+ results = results.filter(
178
+ (s) =>
179
+ s.metadata.name.toLowerCase().includes(query) ||
180
+ s.metadata.description.toLowerCase().includes(query) ||
181
+ s.metadata.tags.some((t) => t.toLowerCase().includes(query))
182
+ );
183
+ }
184
+
185
+ // Sort by relevance (name match first, then description match)
186
+ if (params.query) {
187
+ const query = params.query.toLowerCase();
188
+ results.sort((a, b) => {
189
+ const aNameMatch = a.metadata.name.toLowerCase().includes(query) ? 2 : 0;
190
+ const bNameMatch = b.metadata.name.toLowerCase().includes(query) ? 2 : 0;
191
+ const aDescMatch = a.metadata.description.toLowerCase().includes(query) ? 1 : 0;
192
+ const bDescMatch = b.metadata.description.toLowerCase().includes(query) ? 1 : 0;
193
+ return (bNameMatch + bDescMatch) - (aNameMatch + aDescMatch);
194
+ });
195
+ }
196
+
197
+ // Limit results
198
+ if (params.limit) {
199
+ results = results.slice(0, params.limit);
200
+ }
201
+
202
+ return results;
203
+ }
204
+
205
+ /**
206
+ * List all registered skills.
207
+ */
208
+ async list(): Promise<SkillDefinition[]> {
209
+ if (!this.initialized) {
210
+ await this.initialize();
211
+ }
212
+ return Array.from(this.cache.values());
213
+ }
214
+
215
+ /**
216
+ * Remove a skill.
217
+ */
218
+ async remove(name: string): Promise<boolean> {
219
+ const skillDir = join(this.skillsDir, name);
220
+
221
+ try {
222
+ await rm(skillDir, { recursive: true, force: true });
223
+ } catch {
224
+ return false;
225
+ }
226
+
227
+ this.cache.delete(name);
228
+ return true;
229
+ }
230
+
231
+ /**
232
+ * Get the skills directory path.
233
+ */
234
+ getSkillsDir(): string {
235
+ return this.skillsDir;
236
+ }
237
+
238
+ /**
239
+ * Build the full SKILL.md content with metadata header.
240
+ */
241
+ private buildSkillMd(metadata: SkillMetadata, content: string): string {
242
+ const header = `---
243
+ name: ${metadata.name}
244
+ description: ${metadata.description}
245
+ version: ${metadata.version}
246
+ author: ${metadata.author || 'unknown'}
247
+ tags: ${metadata.tags.join(', ')}
248
+ ${metadata.providers ? `providers: ${metadata.providers.join(', ')}` : ''}
249
+ ${metadata.requiredTools ? `requiredTools: ${metadata.requiredTools.join(', ')}` : ''}
250
+ ${metadata.autoGenerated ? 'autoGenerated: true' : ''}
251
+ createdAt: ${metadata.createdAt}
252
+ updatedAt: ${metadata.updatedAt}
253
+ ---
254
+
255
+ `;
256
+ return header + content;
257
+ }
258
+
259
+ /**
260
+ * Load a skill from disk.
261
+ */
262
+ private async loadSkill(name: string): Promise<SkillDefinition | null> {
263
+ const skillDir = join(this.skillsDir, name);
264
+ const skillMdPath = join(skillDir, 'SKILL.md');
265
+
266
+ try {
267
+ const rawContent = await readFile(skillMdPath, 'utf-8');
268
+ const { metadata, content } = this.parseSkillMd(rawContent);
269
+
270
+ // Load scripts
271
+ const scripts: Record<string, string> = {};
272
+ const scriptsDir = join(skillDir, 'scripts');
273
+ try {
274
+ const entries = await readdir(scriptsDir);
275
+ for (const entry of entries) {
276
+ if (entry.endsWith('.ts') || entry.endsWith('.js') || entry.endsWith('.sh') || entry.endsWith('.py')) {
277
+ scripts[entry] = await readFile(join(scriptsDir, entry), 'utf-8');
278
+ }
279
+ }
280
+ } catch {
281
+ // No scripts directory
282
+ }
283
+
284
+ const fileStat = await stat(skillMdPath);
285
+
286
+ return {
287
+ metadata: { ...metadata, sizeBytes: fileStat.size },
288
+ content,
289
+ scripts: Object.keys(scripts).length > 0 ? scripts : undefined,
290
+ };
291
+ } catch {
292
+ return null;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Parse SKILL.md content into metadata and body.
298
+ */
299
+ private parseSkillMd(rawContent: string): {
300
+ metadata: SkillMetadata;
301
+ content: string;
302
+ } {
303
+ const frontmatterMatch = rawContent.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
304
+ if (!frontmatterMatch) {
305
+ return {
306
+ metadata: {
307
+ name: 'unknown',
308
+ description: 'No description',
309
+ version: '0.0.0',
310
+ tags: [],
311
+ createdAt: new Date().toISOString(),
312
+ updatedAt: new Date().toISOString(),
313
+ },
314
+ content: rawContent,
315
+ };
316
+ }
317
+
318
+ const frontmatter = frontmatterMatch[1];
319
+ const content = frontmatterMatch[2];
320
+
321
+ const parseField = (field: string): string => {
322
+ const match = frontmatter.match(new RegExp(`${field}:\\s*(.+)`));
323
+ return match ? match[1].trim() : '';
324
+ };
325
+
326
+ const parseArray = (field: string): string[] => {
327
+ const raw = parseField(field);
328
+ if (!raw) return [];
329
+ return raw.split(',').map((s) => s.trim()).filter(Boolean);
330
+ };
331
+
332
+ const parseBool = (field: string): boolean => {
333
+ const raw = parseField(field);
334
+ return raw.toLowerCase() === 'true';
335
+ };
336
+
337
+ const name = parseField('name') || 'unknown';
338
+ return {
339
+ metadata: {
340
+ name,
341
+ description: parseField('description'),
342
+ version: parseField('version') || '0.0.0',
343
+ author: parseField('author') || undefined,
344
+ tags: parseArray('tags'),
345
+ providers: parseArray('providers').length > 0 ? parseArray('providers') : undefined,
346
+ requiredTools: parseArray('requiredTools').length > 0 ? parseArray('requiredTools') : undefined,
347
+ autoGenerated: parseBool('autoGenerated'),
348
+ createdAt: parseField('createdAt') || new Date().toISOString(),
349
+ updatedAt: parseField('updatedAt') || new Date().toISOString(),
350
+ },
351
+ content,
352
+ };
353
+ }
354
+
355
+ /**
356
+ * Recursively remove a directory.
357
+ */
358
+ private async removeDirectory(dirPath: string): Promise<void> {
359
+ await rm(dirPath, { recursive: true, force: true });
360
+ }
361
+ }