joonecli 0.1.1 → 0.2.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 (147) hide show
  1. package/dist/cli/index.js +4 -1
  2. package/dist/cli/index.js.map +1 -1
  3. package/dist/commands/builtinCommands.js +6 -6
  4. package/dist/commands/builtinCommands.js.map +1 -1
  5. package/dist/commands/commandRegistry.d.ts +3 -1
  6. package/dist/commands/commandRegistry.js.map +1 -1
  7. package/dist/core/agentLoop.d.ts +3 -1
  8. package/dist/core/agentLoop.js +17 -7
  9. package/dist/core/agentLoop.js.map +1 -1
  10. package/dist/core/compactor.js +2 -2
  11. package/dist/core/compactor.js.map +1 -1
  12. package/dist/core/contextGuard.d.ts +5 -0
  13. package/dist/core/contextGuard.js +30 -3
  14. package/dist/core/contextGuard.js.map +1 -1
  15. package/dist/core/events.d.ts +45 -0
  16. package/dist/core/events.js +8 -0
  17. package/dist/core/events.js.map +1 -0
  18. package/dist/core/sessionStore.js +3 -2
  19. package/dist/core/sessionStore.js.map +1 -1
  20. package/dist/core/subAgent.js +2 -2
  21. package/dist/core/subAgent.js.map +1 -1
  22. package/dist/core/tokenCounter.d.ts +8 -1
  23. package/dist/core/tokenCounter.js +28 -0
  24. package/dist/core/tokenCounter.js.map +1 -1
  25. package/dist/middleware/permission.js +1 -0
  26. package/dist/middleware/permission.js.map +1 -1
  27. package/dist/tools/browser.js +4 -1
  28. package/dist/tools/browser.js.map +1 -1
  29. package/dist/tools/index.d.ts +2 -1
  30. package/dist/tools/index.js +11 -3
  31. package/dist/tools/index.js.map +1 -1
  32. package/dist/tools/installHostDeps.d.ts +2 -0
  33. package/dist/tools/installHostDeps.js +37 -0
  34. package/dist/tools/installHostDeps.js.map +1 -0
  35. package/dist/tools/router.js +1 -0
  36. package/dist/tools/router.js.map +1 -1
  37. package/dist/tools/spawnAgent.js +3 -1
  38. package/dist/tools/spawnAgent.js.map +1 -1
  39. package/dist/tracing/sessionTracer.d.ts +1 -0
  40. package/dist/tracing/sessionTracer.js +4 -1
  41. package/dist/tracing/sessionTracer.js.map +1 -1
  42. package/dist/ui/App.js +6 -1
  43. package/dist/ui/App.js.map +1 -1
  44. package/dist/ui/components/ActionLog.d.ts +7 -0
  45. package/dist/ui/components/ActionLog.js +63 -0
  46. package/dist/ui/components/ActionLog.js.map +1 -0
  47. package/dist/ui/components/FileBrowser.d.ts +2 -0
  48. package/dist/ui/components/FileBrowser.js +41 -0
  49. package/dist/ui/components/FileBrowser.js.map +1 -0
  50. package/package.json +3 -5
  51. package/AGENTS.md +0 -56
  52. package/Handover.md +0 -115
  53. package/PROGRESS.md +0 -160
  54. package/docs/01_insights_and_patterns.md +0 -27
  55. package/docs/02_edge_cases_and_mitigations.md +0 -143
  56. package/docs/03_initial_implementation_plan.md +0 -66
  57. package/docs/04_tech_stack_proposal.md +0 -20
  58. package/docs/05_prd.md +0 -87
  59. package/docs/06_user_stories.md +0 -72
  60. package/docs/07_system_architecture.md +0 -138
  61. package/docs/08_roadmap.md +0 -200
  62. package/e2b/Dockerfile +0 -26
  63. package/src/__tests__/bootstrap.test.ts +0 -111
  64. package/src/__tests__/config.test.ts +0 -97
  65. package/src/__tests__/m55.test.ts +0 -238
  66. package/src/__tests__/middleware.test.ts +0 -219
  67. package/src/__tests__/modelFactory.test.ts +0 -63
  68. package/src/__tests__/optimizations.test.ts +0 -201
  69. package/src/__tests__/promptBuilder.test.ts +0 -141
  70. package/src/__tests__/sandbox.test.ts +0 -102
  71. package/src/__tests__/security.test.ts +0 -122
  72. package/src/__tests__/streaming.test.ts +0 -82
  73. package/src/__tests__/toolRouter.test.ts +0 -52
  74. package/src/__tests__/tools.test.ts +0 -146
  75. package/src/__tests__/tracing.test.ts +0 -196
  76. package/src/agents/agentRegistry.ts +0 -69
  77. package/src/agents/agentSpec.ts +0 -67
  78. package/src/agents/builtinAgents.ts +0 -142
  79. package/src/cli/config.ts +0 -124
  80. package/src/cli/index.ts +0 -742
  81. package/src/cli/modelFactory.ts +0 -174
  82. package/src/cli/postinstall.ts +0 -28
  83. package/src/cli/providers.ts +0 -107
  84. package/src/commands/builtinCommands.ts +0 -293
  85. package/src/commands/commandRegistry.ts +0 -194
  86. package/src/core/agentLoop.d.ts.map +0 -1
  87. package/src/core/agentLoop.ts +0 -312
  88. package/src/core/autoSave.ts +0 -95
  89. package/src/core/compactor.ts +0 -252
  90. package/src/core/contextGuard.ts +0 -129
  91. package/src/core/errors.ts +0 -202
  92. package/src/core/promptBuilder.d.ts.map +0 -1
  93. package/src/core/promptBuilder.ts +0 -139
  94. package/src/core/reasoningRouter.ts +0 -121
  95. package/src/core/retry.ts +0 -75
  96. package/src/core/sessionResumer.ts +0 -90
  97. package/src/core/sessionStore.ts +0 -216
  98. package/src/core/subAgent.ts +0 -339
  99. package/src/core/tokenCounter.ts +0 -64
  100. package/src/evals/dataset.ts +0 -67
  101. package/src/evals/evaluator.ts +0 -81
  102. package/src/hitl/bridge.ts +0 -160
  103. package/src/middleware/commandSanitizer.ts +0 -60
  104. package/src/middleware/loopDetection.ts +0 -63
  105. package/src/middleware/permission.ts +0 -72
  106. package/src/middleware/pipeline.ts +0 -75
  107. package/src/middleware/preCompletion.ts +0 -94
  108. package/src/middleware/types.ts +0 -45
  109. package/src/sandbox/bootstrap.ts +0 -121
  110. package/src/sandbox/manager.ts +0 -239
  111. package/src/sandbox/sync.ts +0 -157
  112. package/src/skills/loader.ts +0 -143
  113. package/src/skills/tools.ts +0 -99
  114. package/src/skills/types.ts +0 -13
  115. package/src/test_cache.ts +0 -72
  116. package/src/tools/askUser.ts +0 -47
  117. package/src/tools/browser.ts +0 -137
  118. package/src/tools/index.d.ts.map +0 -1
  119. package/src/tools/index.ts +0 -237
  120. package/src/tools/registry.ts +0 -198
  121. package/src/tools/router.ts +0 -78
  122. package/src/tools/security.ts +0 -220
  123. package/src/tools/spawnAgent.ts +0 -158
  124. package/src/tools/webSearch.ts +0 -142
  125. package/src/tracing/analyzer.ts +0 -265
  126. package/src/tracing/langsmith.ts +0 -63
  127. package/src/tracing/sessionTracer.ts +0 -202
  128. package/src/tracing/types.ts +0 -49
  129. package/src/types/valyu.d.ts +0 -37
  130. package/src/ui/App.tsx +0 -404
  131. package/src/ui/components/HITLPrompt.tsx +0 -119
  132. package/src/ui/components/Header.tsx +0 -51
  133. package/src/ui/components/MessageBubble.tsx +0 -46
  134. package/src/ui/components/StatusBar.tsx +0 -138
  135. package/src/ui/components/StreamingText.tsx +0 -48
  136. package/src/ui/components/ToolCallPanel.tsx +0 -80
  137. package/tests/commands/commands.test.ts +0 -356
  138. package/tests/core/compactor.test.ts +0 -217
  139. package/tests/core/retryAndErrors.test.ts +0 -164
  140. package/tests/core/sessionResumer.test.ts +0 -95
  141. package/tests/core/sessionStore.test.ts +0 -84
  142. package/tests/core/stability.test.ts +0 -165
  143. package/tests/core/subAgent.test.ts +0 -238
  144. package/tests/hitl/hitlBridge.test.ts +0 -115
  145. package/tsconfig.json +0 -16
  146. package/vitest.config.ts +0 -10
  147. package/vitest.out +0 -48
@@ -1,239 +0,0 @@
1
- import { Sandbox as E2BSandbox } from "e2b";
2
- import { Sandbox as OSandbox, ConnectionConfig } from "@alibaba-group/opensandbox";
3
-
4
- /**
5
- * Result of a command execution in the sandbox.
6
- */
7
- export interface CommandResult {
8
- stdout: string;
9
- stderr: string;
10
- exitCode: number;
11
- }
12
-
13
- /**
14
- * Options for creating a SandboxManager.
15
- */
16
- export interface SandboxManagerOptions {
17
- /** E2B API key. If not provided, reads from E2B_API_KEY env var. */
18
- apiKey?: string;
19
- /** OpenSandbox API key for the fallback. If not provided, reads from OPENSANDBOX_API_KEY env var. */
20
- openSandboxApiKey?: string;
21
- /** OpenSandbox API domain connection string. Defaults to localhost:8080. */
22
- openSandboxDomain?: string;
23
- /** Sandbox template to use. Defaults to E2B's base template. */
24
- template?: string;
25
- /** Sandbox timeout in milliseconds. Defaults to 5 minutes. */
26
- timeoutMs?: number;
27
- }
28
-
29
- /**
30
- * Unifies E2B and OpenSandbox under a single interface.
31
- */
32
- interface ISandboxWrapper {
33
- kill(): Promise<void>;
34
- exec(command: string): Promise<CommandResult>;
35
- uploadFile(path: string, content: string): Promise<void>;
36
- getInternalInstance(): E2BSandbox | OSandbox;
37
- getId(): string;
38
- getProviderName(): "e2b" | "opensandbox";
39
- }
40
-
41
- class E2BSandboxWrapper implements ISandboxWrapper {
42
- constructor(private sandbox: E2BSandbox) {}
43
-
44
- async kill(): Promise<void> {
45
- await this.sandbox.kill();
46
- }
47
-
48
- async exec(command: string): Promise<CommandResult> {
49
- const result = await this.sandbox.commands.run(command);
50
- return {
51
- stdout: result.stdout,
52
- stderr: result.stderr,
53
- exitCode: result.exitCode,
54
- };
55
- }
56
-
57
- async uploadFile(path: string, content: string): Promise<void> {
58
- await this.sandbox.files.write(path, content);
59
- }
60
-
61
- getInternalInstance(): E2BSandbox {
62
- return this.sandbox;
63
- }
64
-
65
- getId(): string {
66
- return this.sandbox.sandboxId;
67
- }
68
-
69
- getProviderName(): "e2b" | "opensandbox" {
70
- return "e2b";
71
- }
72
- }
73
-
74
- class OpenSandboxWrapper implements ISandboxWrapper {
75
- constructor(private sandbox: OSandbox) {}
76
-
77
- async kill(): Promise<void> {
78
- await this.sandbox.kill();
79
- }
80
-
81
- async exec(command: string): Promise<CommandResult> {
82
- const execution = await this.sandbox.commands.run(command);
83
-
84
- // Process logs. OpenSandbox separates output into arrays of messages.
85
- const stdout = execution.logs.stdout.map(m => m.text).join("");
86
- const stderr = execution.logs.stderr.map(m => m.text).join("");
87
-
88
- // If there is an error block on the execution response, it means it failed.
89
- const exitCode = execution.error ? 1 : 0;
90
-
91
- return { stdout, stderr, exitCode };
92
- }
93
-
94
- async uploadFile(path: string, content: string): Promise<void> {
95
- await this.sandbox.files.writeFiles([{
96
- path,
97
- data: content,
98
- mode: 0o644
99
- }]);
100
- }
101
-
102
- getInternalInstance(): OSandbox {
103
- return this.sandbox;
104
- }
105
-
106
- getId(): string {
107
- return this.sandbox.id || "opensandbox-local";
108
- }
109
-
110
- getProviderName(): "e2b" | "opensandbox" {
111
- return "opensandbox";
112
- }
113
- }
114
-
115
- /**
116
- * Manages the lifecycle of a cloud or local sandbox.
117
- * Attempts to use E2B first. If E2B fails (e.g., networking/auth error),
118
- * it automatically falls back to OpenSandbox (local docker).
119
- */
120
- export class SandboxManager {
121
- private wrapper: ISandboxWrapper | null = null;
122
- private readonly e2bApiKey?: string;
123
- private readonly osApiKey?: string;
124
- private readonly osDomain?: string;
125
- private readonly template?: string;
126
- private readonly timeoutMs: number;
127
-
128
- constructor(opts: SandboxManagerOptions = {}) {
129
- this.e2bApiKey = opts.apiKey;
130
- this.osApiKey = opts.openSandboxApiKey;
131
- this.osDomain = opts.openSandboxDomain; // defaults via SDK if undefined
132
- this.template = opts.template;
133
- this.timeoutMs = opts.timeoutMs ?? 300_000; // 5 minutes default
134
- }
135
-
136
- /**
137
- * Creates a new sandboxed execution environment.
138
- * @returns The sandbox ID.
139
- */
140
- async create(): Promise<string> {
141
- if (this.wrapper) {
142
- await this.destroy();
143
- }
144
-
145
- // 1. Attempt E2B Primary Sandbox
146
- try {
147
- const createOpts = {
148
- apiKey: this.e2bApiKey,
149
- timeoutMs: this.timeoutMs,
150
- };
151
-
152
- const e2bInstance = this.template
153
- ? await E2BSandbox.create(this.template, createOpts)
154
- : await E2BSandbox.create(createOpts);
155
-
156
- this.wrapper = new E2BSandboxWrapper(e2bInstance);
157
- return this.wrapper.getId();
158
-
159
- } catch (e2bError: any) {
160
- console.warn(`[SandboxManager] E2B initialization failed: ${e2bError.message}. Falling back to OpenSandbox...`);
161
-
162
- // 2. Attempt OpenSandbox Fallback
163
- try {
164
- const configOpts: { apiKey?: string; domain?: string } = {};
165
- if (this.osApiKey) configOpts.apiKey = this.osApiKey;
166
- if (this.osDomain) configOpts.domain = this.osDomain;
167
- // else defaults locally to localhost:8080 without API key (Docker setup)
168
-
169
- const config = new ConnectionConfig(configOpts);
170
- const osInstance = await OSandbox.create({
171
- connectionConfig: config,
172
- image: this.template || "ubuntu:22.04", // Fallback to basic ubuntu if E2B template is specified
173
- timeoutSeconds: Math.floor(this.timeoutMs / 1000)
174
- });
175
-
176
- this.wrapper = new OpenSandboxWrapper(osInstance);
177
- return this.wrapper.getId();
178
-
179
- } catch (osError: any) {
180
- throw new Error(`CRITICAL: Both E2B and OpenSandbox initialization failed.\nE2B Error: ${e2bError.message}\nOpenSandbox Error: ${osError.message}`);
181
- }
182
- }
183
- }
184
-
185
- /**
186
- * Destroys the active sandbox.
187
- */
188
- async destroy(): Promise<void> {
189
- if (this.wrapper) {
190
- await this.wrapper.kill();
191
- this.wrapper = null;
192
- }
193
- }
194
-
195
- /**
196
- * Returns whether a sandbox is currently active.
197
- */
198
- isActive(): boolean {
199
- return this.wrapper !== null;
200
- }
201
-
202
- /**
203
- * Executes a shell command in the sandbox.
204
- *
205
- * @param command The shell command to run.
206
- * @returns The command result (stdout, stderr, exitCode).
207
- * @throws If the sandbox is not active.
208
- */
209
- async exec(command: string): Promise<CommandResult> {
210
- if (!this.wrapper) {
211
- throw new Error("Sandbox is not active. Call create() before exec().");
212
- }
213
- return await this.wrapper.exec(command);
214
- }
215
-
216
- /**
217
- * Uploads a file to the sandbox filesystem.
218
- *
219
- * @param filePath Absolute path inside the sandbox (e.g., "/workspace/src/foo.ts").
220
- * @param content File content as a string.
221
- */
222
- async uploadFile(filePath: string, content: string): Promise<void> {
223
- if (!this.wrapper) {
224
- throw new Error("Sandbox is not active. Call create() before uploadFile().");
225
- }
226
- await this.wrapper.uploadFile(filePath, content);
227
- }
228
-
229
- /**
230
- * Returns the underlying E2B or OpenSandbox instance for advanced operations.
231
- * @throws If the sandbox is not active.
232
- */
233
- getSandbox(): E2BSandbox | OSandbox {
234
- if (!this.wrapper) {
235
- throw new Error("Sandbox is not active.");
236
- }
237
- return this.wrapper.getInternalInstance();
238
- }
239
- }
@@ -1,157 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { SandboxManager } from "./manager.js";
4
-
5
- /**
6
- * Tracks file changes on the host and syncs them to the sandbox.
7
- *
8
- * Strategy: Upload-on-execute.
9
- * Before each sandbox command, only changed files are uploaded.
10
- */
11
- export class FileSync {
12
- /** Files that have been modified since the last sync. */
13
- private dirtyFiles = new Map<string, string>(); // hostPath → sandboxPath
14
-
15
- /** The base directory on the host that maps to /workspace in sandbox. */
16
- private readonly projectRoot: string;
17
-
18
- /** The base path inside the sandbox. */
19
- private readonly sandboxRoot: string;
20
-
21
- constructor(projectRoot: string, sandboxRoot: string = "/workspace") {
22
- this.projectRoot = projectRoot;
23
- this.sandboxRoot = sandboxRoot;
24
- }
25
-
26
- /**
27
- * Mark a file as dirty (changed on host, needs sync to sandbox).
28
- *
29
- * @param hostPath Absolute path on the host filesystem.
30
- */
31
- markDirty(hostPath: string): void {
32
- const relative = path.relative(this.projectRoot, hostPath);
33
- const sandboxPath = path.posix.join(this.sandboxRoot, relative.replace(/\\/g, "/"));
34
- this.dirtyFiles.set(hostPath, sandboxPath);
35
- }
36
-
37
- /**
38
- * Returns the number of files pending sync.
39
- */
40
- pendingCount(): number {
41
- return this.dirtyFiles.size;
42
- }
43
-
44
- /**
45
- * Syncs all dirty files to the sandbox.
46
- * Reads each file from the host and uploads it to the sandbox.
47
- *
48
- * @param sandbox The active SandboxManager.
49
- * @returns Number of files synced.
50
- */
51
- async syncToSandbox(sandbox: SandboxManager): Promise<number> {
52
- let synced = 0;
53
-
54
- for (const [hostPath, sandboxPath] of this.dirtyFiles) {
55
- try {
56
- const content = fs.readFileSync(hostPath, "utf-8");
57
- await sandbox.uploadFile(sandboxPath, content);
58
- synced++;
59
- } catch (error: any) {
60
- if (error.code === "ENOENT") {
61
- // File was deleted from the host before it could be synced.
62
- // We safely ignore this to prevent crashing the sync loop.
63
- } else {
64
- console.error(`Error reading ${hostPath} for sync:`, error.message);
65
- }
66
- }
67
- }
68
-
69
- this.dirtyFiles.clear();
70
- return synced;
71
- }
72
-
73
- /**
74
- * Performs an initial full sync of the project directory.
75
- * Uploads all files (excluding node_modules, .git, dist).
76
- *
77
- * @param sandbox The active SandboxManager.
78
- * @returns Number of files synced.
79
- */
80
- async initialSync(sandbox: SandboxManager): Promise<number> {
81
- const EXCLUDE_DIRS = new Set(["node_modules", ".git", "dist", ".next", "__pycache__"]);
82
- let synced = 0;
83
-
84
- const walkDir = (dir: string) => {
85
- const entries = fs.readdirSync(dir, { withFileTypes: true });
86
-
87
- for (const entry of entries) {
88
- const fullPath = path.join(dir, entry.name);
89
-
90
- if (entry.isDirectory()) {
91
- if (!EXCLUDE_DIRS.has(entry.name)) {
92
- walkDir(fullPath);
93
- }
94
- } else if (entry.isFile()) {
95
- this.markDirty(fullPath);
96
- }
97
- }
98
- };
99
-
100
- walkDir(this.projectRoot);
101
- synced = await this.syncToSandbox(sandbox);
102
- return synced;
103
- }
104
-
105
- /**
106
- * Syncs user-level skill directories into the sandbox.
107
- * Skills from `~/.joone/skills/` and `~/.agents/skills/` are uploaded
108
- * into `/workspace/.joone/skills/` so they are available for agent execution.
109
- *
110
- * @param sandbox The active SandboxManager.
111
- * @param skillPaths Absolute host paths to skill directories to sync.
112
- * @returns Number of files synced.
113
- */
114
- async syncSkillsToSandbox(
115
- sandbox: SandboxManager,
116
- skillPaths: { path: string; source: "project" | "user" }[]
117
- ): Promise<number> {
118
- let synced = 0;
119
-
120
- for (const { path: skillDir, source } of skillPaths) {
121
- // Only sync user-level skills (project-level are already in projectRoot)
122
- if (source !== "user") continue;
123
- if (!fs.existsSync(skillDir)) continue;
124
-
125
- const walkSkillDir = (dir: string) => {
126
- let entries: fs.Dirent[];
127
- try {
128
- entries = fs.readdirSync(dir, { withFileTypes: true });
129
- } catch {
130
- return;
131
- }
132
-
133
- for (const entry of entries) {
134
- const fullPath = path.join(dir, entry.name);
135
-
136
- if (entry.isDirectory()) {
137
- walkSkillDir(fullPath);
138
- } else if (entry.isFile()) {
139
- const relative = path.relative(skillDir, fullPath);
140
- const sandboxPath = path.posix.join(
141
- this.sandboxRoot,
142
- ".joone",
143
- "skills",
144
- relative.replace(/\\/g, "/")
145
- );
146
- this.dirtyFiles.set(fullPath, sandboxPath);
147
- }
148
- }
149
- };
150
-
151
- walkSkillDir(skillDir);
152
- }
153
-
154
- synced = await this.syncToSandbox(sandbox);
155
- return synced;
156
- }
157
- }
@@ -1,143 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import * as os from "node:os";
4
- import { SkillMeta } from "./types.js";
5
-
6
- /**
7
- * SkillLoader — discovers and loads skills from multiple directories.
8
- *
9
- * Discovery paths (priority order — project overrides user):
10
- * 1. Project root: ./skills/, ./.agents/skills/, ./.agent/skills/
11
- * 2. User home: ~/.joone/skills/, ~/.agents/skills/
12
- *
13
- * On Windows, ~ resolves to %USERPROFILE%.
14
- *
15
- * Skills are folders containing a SKILL.md with YAML frontmatter:
16
- * ---
17
- * name: my-skill
18
- * description: What this skill does
19
- * ---
20
- * ## Instructions
21
- * ...
22
- */
23
- export class SkillLoader {
24
- private projectRoot: string;
25
- private userHome: string;
26
- private cachedSkills: SkillMeta[] | null = null;
27
-
28
- constructor(projectRoot?: string, userHome?: string) {
29
- this.projectRoot = projectRoot ?? process.cwd();
30
- this.userHome = userHome ?? os.homedir();
31
- }
32
-
33
- /**
34
- * Returns all skill discovery directories in priority order.
35
- * Project-level directories come first (higher priority).
36
- */
37
- getDiscoveryPaths(): { path: string; source: "project" | "user" }[] {
38
- const home = this.userHome;
39
-
40
- return [
41
- // Project-level (highest priority)
42
- { path: path.join(this.projectRoot, "skills"), source: "project" as const },
43
- { path: path.join(this.projectRoot, ".agents", "skills"), source: "project" as const },
44
- { path: path.join(this.projectRoot, ".agent", "skills"), source: "project" as const },
45
- // User-level
46
- { path: path.join(home, ".joone", "skills"), source: "user" as const },
47
- { path: path.join(home, ".agents", "skills"), source: "user" as const },
48
- ];
49
- }
50
-
51
- /**
52
- * Parses YAML frontmatter from a SKILL.md content string.
53
- * Simple parser — handles `name:` and `description:` fields.
54
- */
55
- parseFrontmatter(content: string): { name?: string; description?: string } {
56
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
57
- if (!match) return {};
58
-
59
- const yaml = match[1];
60
- const nameMatch = yaml.match(/^name:\s*(.+)$/m);
61
- const descMatch = yaml.match(/^description:\s*(.+)$/m);
62
-
63
- return {
64
- name: nameMatch?.[1]?.trim(),
65
- description: descMatch?.[1]?.trim(),
66
- };
67
- }
68
-
69
- /**
70
- * Discovers all available skills from all discovery paths.
71
- * Deduplicates by name — project-level skills override user-level.
72
- * Results are cached per session.
73
- */
74
- discoverSkills(): SkillMeta[] {
75
- if (this.cachedSkills) return this.cachedSkills;
76
-
77
- const skills = new Map<string, SkillMeta>();
78
- const paths = this.getDiscoveryPaths();
79
-
80
- for (const { path: dir, source } of paths) {
81
- if (!fs.existsSync(dir)) continue;
82
-
83
- let entries: fs.Dirent[];
84
- try {
85
- entries = fs.readdirSync(dir, { withFileTypes: true });
86
- } catch {
87
- continue;
88
- }
89
-
90
- for (const entry of entries) {
91
- if (!entry.isDirectory()) continue;
92
-
93
- const skillMdPath = path.join(dir, entry.name, "SKILL.md");
94
- if (!fs.existsSync(skillMdPath)) continue;
95
-
96
- try {
97
- const content = fs.readFileSync(skillMdPath, "utf-8");
98
- const frontmatter = this.parseFrontmatter(content);
99
-
100
- const name = frontmatter.name || entry.name;
101
-
102
- // Only add if not already found (project overrides user)
103
- if (!skills.has(name)) {
104
- skills.set(name, {
105
- name,
106
- description: frontmatter.description || `Skill: ${name}`,
107
- path: skillMdPath,
108
- source,
109
- });
110
- }
111
- } catch {
112
- // Skip unreadable skills
113
- }
114
- }
115
- }
116
-
117
- this.cachedSkills = Array.from(skills.values());
118
- return this.cachedSkills;
119
- }
120
-
121
- /**
122
- * Loads the full content of a specific skill's SKILL.md.
123
- * Returns undefined if the skill is not found.
124
- */
125
- loadSkill(name: string): string | undefined {
126
- const skills = this.discoverSkills();
127
- const skill = skills.find((s) => s.name === name);
128
- if (!skill) return undefined;
129
-
130
- try {
131
- return fs.readFileSync(skill.path, "utf-8");
132
- } catch {
133
- return undefined;
134
- }
135
- }
136
-
137
- /**
138
- * Clears the cached skills. Call when skills directory contents may have changed.
139
- */
140
- clearCache(): void {
141
- this.cachedSkills = null;
142
- }
143
- }
@@ -1,99 +0,0 @@
1
- import { DynamicToolInterface, ToolResult } from "../tools/index.js";
2
- import { SkillLoader } from "./loader.js";
3
-
4
- // ─── Shared SkillLoader instance ──────────────────────────────────────────────
5
-
6
- let _loader: SkillLoader | null = null;
7
-
8
- export function bindSkillLoader(loader: SkillLoader): void {
9
- _loader = loader;
10
- }
11
-
12
- function getLoader(): SkillLoader {
13
- if (!_loader) {
14
- _loader = new SkillLoader();
15
- }
16
- return _loader;
17
- }
18
-
19
- // ─── SearchSkillsTool ───────────────────────────────────────────────────────────
20
-
21
- export const SearchSkillsTool: DynamicToolInterface = {
22
- name: "search_skills",
23
- description:
24
- "Search for available skills. Skills provide specialized instructions for specific tasks " +
25
- "(e.g., deployment workflows, testing strategies, coding patterns).",
26
- schema: {
27
- type: "object",
28
- properties: {
29
- query: {
30
- type: "string",
31
- description:
32
- "Search query to match against skill names and descriptions (optional — omit to list all)",
33
- },
34
- },
35
- },
36
- execute: async (args?: { query?: string }): Promise<ToolResult> => {
37
- const loader = getLoader();
38
- let skills = loader.discoverSkills();
39
-
40
- if (args?.query) {
41
- const q = args.query.toLowerCase();
42
- skills = skills.filter(
43
- (s) =>
44
- s.name.toLowerCase().includes(q) ||
45
- s.description.toLowerCase().includes(q)
46
- );
47
- }
48
-
49
- if (skills.length === 0) {
50
- return {
51
- content: args?.query
52
- ? `No skills found matching "${args.query}".`
53
- : "No skills found. Create a skill by adding a folder with SKILL.md to ./skills/ or ~/.joone/skills/."
54
- };
55
- }
56
-
57
- const list = skills
58
- .map((s) => `- **${s.name}** (${s.source}): ${s.description}`)
59
- .join("\n");
60
-
61
- return {
62
- content:
63
- `Found ${skills.length} skill(s):\n${list}\n\n` +
64
- `To load a skill, call \`load_skill\` with the skill name.`
65
- };
66
- },
67
- };
68
-
69
- // ─── LoadSkillTool ──────────────────────────────────────────────────────────────
70
-
71
- export const LoadSkillTool: DynamicToolInterface = {
72
- name: "load_skill",
73
- description:
74
- "Loads a specific skill's full instructions (SKILL.md content). " +
75
- "Use search_skills first to discover available skills.",
76
- schema: {
77
- type: "object",
78
- properties: {
79
- name: {
80
- type: "string",
81
- description: "The name of the skill to load",
82
- },
83
- },
84
- required: ["name"],
85
- },
86
- execute: async (args: { name: string }): Promise<ToolResult> => {
87
- const loader = getLoader();
88
- const content = loader.loadSkill(args.name);
89
-
90
- if (!content) {
91
- return {
92
- content: `Error: Skill "${args.name}" not found. Use search_skills to see available skills.`,
93
- isError: true
94
- };
95
- }
96
-
97
- return { content };
98
- },
99
- };
@@ -1,13 +0,0 @@
1
- /**
2
- * Skill metadata parsed from SKILL.md YAML frontmatter.
3
- */
4
- export interface SkillMeta {
5
- /** Human-readable name (from YAML `name` field). */
6
- name: string;
7
- /** Short description (from YAML `description` field). */
8
- description: string;
9
- /** Absolute path to the SKILL.md file. */
10
- path: string;
11
- /** Where this skill was discovered. */
12
- source: "project" | "user";
13
- }