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.
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/builtinCommands.js +6 -6
- package/dist/commands/builtinCommands.js.map +1 -1
- package/dist/commands/commandRegistry.d.ts +3 -1
- package/dist/commands/commandRegistry.js.map +1 -1
- package/dist/core/agentLoop.d.ts +3 -1
- package/dist/core/agentLoop.js +17 -7
- package/dist/core/agentLoop.js.map +1 -1
- package/dist/core/compactor.js +2 -2
- package/dist/core/compactor.js.map +1 -1
- package/dist/core/contextGuard.d.ts +5 -0
- package/dist/core/contextGuard.js +30 -3
- package/dist/core/contextGuard.js.map +1 -1
- package/dist/core/events.d.ts +45 -0
- package/dist/core/events.js +8 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/sessionStore.js +3 -2
- package/dist/core/sessionStore.js.map +1 -1
- package/dist/core/subAgent.js +2 -2
- package/dist/core/subAgent.js.map +1 -1
- package/dist/core/tokenCounter.d.ts +8 -1
- package/dist/core/tokenCounter.js +28 -0
- package/dist/core/tokenCounter.js.map +1 -1
- package/dist/middleware/permission.js +1 -0
- package/dist/middleware/permission.js.map +1 -1
- package/dist/tools/browser.js +4 -1
- package/dist/tools/browser.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +11 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/installHostDeps.d.ts +2 -0
- package/dist/tools/installHostDeps.js +37 -0
- package/dist/tools/installHostDeps.js.map +1 -0
- package/dist/tools/router.js +1 -0
- package/dist/tools/router.js.map +1 -1
- package/dist/tools/spawnAgent.js +3 -1
- package/dist/tools/spawnAgent.js.map +1 -1
- package/dist/tracing/sessionTracer.d.ts +1 -0
- package/dist/tracing/sessionTracer.js +4 -1
- package/dist/tracing/sessionTracer.js.map +1 -1
- package/dist/ui/App.js +6 -1
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/ActionLog.d.ts +7 -0
- package/dist/ui/components/ActionLog.js +63 -0
- package/dist/ui/components/ActionLog.js.map +1 -0
- package/dist/ui/components/FileBrowser.d.ts +2 -0
- package/dist/ui/components/FileBrowser.js +41 -0
- package/dist/ui/components/FileBrowser.js.map +1 -0
- package/package.json +3 -5
- package/AGENTS.md +0 -56
- package/Handover.md +0 -115
- package/PROGRESS.md +0 -160
- package/docs/01_insights_and_patterns.md +0 -27
- package/docs/02_edge_cases_and_mitigations.md +0 -143
- package/docs/03_initial_implementation_plan.md +0 -66
- package/docs/04_tech_stack_proposal.md +0 -20
- package/docs/05_prd.md +0 -87
- package/docs/06_user_stories.md +0 -72
- package/docs/07_system_architecture.md +0 -138
- package/docs/08_roadmap.md +0 -200
- package/e2b/Dockerfile +0 -26
- package/src/__tests__/bootstrap.test.ts +0 -111
- package/src/__tests__/config.test.ts +0 -97
- package/src/__tests__/m55.test.ts +0 -238
- package/src/__tests__/middleware.test.ts +0 -219
- package/src/__tests__/modelFactory.test.ts +0 -63
- package/src/__tests__/optimizations.test.ts +0 -201
- package/src/__tests__/promptBuilder.test.ts +0 -141
- package/src/__tests__/sandbox.test.ts +0 -102
- package/src/__tests__/security.test.ts +0 -122
- package/src/__tests__/streaming.test.ts +0 -82
- package/src/__tests__/toolRouter.test.ts +0 -52
- package/src/__tests__/tools.test.ts +0 -146
- package/src/__tests__/tracing.test.ts +0 -196
- package/src/agents/agentRegistry.ts +0 -69
- package/src/agents/agentSpec.ts +0 -67
- package/src/agents/builtinAgents.ts +0 -142
- package/src/cli/config.ts +0 -124
- package/src/cli/index.ts +0 -742
- package/src/cli/modelFactory.ts +0 -174
- package/src/cli/postinstall.ts +0 -28
- package/src/cli/providers.ts +0 -107
- package/src/commands/builtinCommands.ts +0 -293
- package/src/commands/commandRegistry.ts +0 -194
- package/src/core/agentLoop.d.ts.map +0 -1
- package/src/core/agentLoop.ts +0 -312
- package/src/core/autoSave.ts +0 -95
- package/src/core/compactor.ts +0 -252
- package/src/core/contextGuard.ts +0 -129
- package/src/core/errors.ts +0 -202
- package/src/core/promptBuilder.d.ts.map +0 -1
- package/src/core/promptBuilder.ts +0 -139
- package/src/core/reasoningRouter.ts +0 -121
- package/src/core/retry.ts +0 -75
- package/src/core/sessionResumer.ts +0 -90
- package/src/core/sessionStore.ts +0 -216
- package/src/core/subAgent.ts +0 -339
- package/src/core/tokenCounter.ts +0 -64
- package/src/evals/dataset.ts +0 -67
- package/src/evals/evaluator.ts +0 -81
- package/src/hitl/bridge.ts +0 -160
- package/src/middleware/commandSanitizer.ts +0 -60
- package/src/middleware/loopDetection.ts +0 -63
- package/src/middleware/permission.ts +0 -72
- package/src/middleware/pipeline.ts +0 -75
- package/src/middleware/preCompletion.ts +0 -94
- package/src/middleware/types.ts +0 -45
- package/src/sandbox/bootstrap.ts +0 -121
- package/src/sandbox/manager.ts +0 -239
- package/src/sandbox/sync.ts +0 -157
- package/src/skills/loader.ts +0 -143
- package/src/skills/tools.ts +0 -99
- package/src/skills/types.ts +0 -13
- package/src/test_cache.ts +0 -72
- package/src/tools/askUser.ts +0 -47
- package/src/tools/browser.ts +0 -137
- package/src/tools/index.d.ts.map +0 -1
- package/src/tools/index.ts +0 -237
- package/src/tools/registry.ts +0 -198
- package/src/tools/router.ts +0 -78
- package/src/tools/security.ts +0 -220
- package/src/tools/spawnAgent.ts +0 -158
- package/src/tools/webSearch.ts +0 -142
- package/src/tracing/analyzer.ts +0 -265
- package/src/tracing/langsmith.ts +0 -63
- package/src/tracing/sessionTracer.ts +0 -202
- package/src/tracing/types.ts +0 -49
- package/src/types/valyu.d.ts +0 -37
- package/src/ui/App.tsx +0 -404
- package/src/ui/components/HITLPrompt.tsx +0 -119
- package/src/ui/components/Header.tsx +0 -51
- package/src/ui/components/MessageBubble.tsx +0 -46
- package/src/ui/components/StatusBar.tsx +0 -138
- package/src/ui/components/StreamingText.tsx +0 -48
- package/src/ui/components/ToolCallPanel.tsx +0 -80
- package/tests/commands/commands.test.ts +0 -356
- package/tests/core/compactor.test.ts +0 -217
- package/tests/core/retryAndErrors.test.ts +0 -164
- package/tests/core/sessionResumer.test.ts +0 -95
- package/tests/core/sessionStore.test.ts +0 -84
- package/tests/core/stability.test.ts +0 -165
- package/tests/core/subAgent.test.ts +0 -238
- package/tests/hitl/hitlBridge.test.ts +0 -115
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -10
- package/vitest.out +0 -48
package/src/sandbox/manager.ts
DELETED
|
@@ -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
|
-
}
|
package/src/sandbox/sync.ts
DELETED
|
@@ -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
|
-
}
|
package/src/skills/loader.ts
DELETED
|
@@ -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
|
-
}
|
package/src/skills/tools.ts
DELETED
|
@@ -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
|
-
};
|
package/src/skills/types.ts
DELETED
|
@@ -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
|
-
}
|