joonecli 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.
- package/AGENTS.md +56 -0
- package/Handover.md +115 -0
- package/LICENSE +201 -0
- package/PROGRESS.md +160 -0
- package/README.md +114 -0
- package/dist/__tests__/bootstrap.test.d.ts +1 -0
- package/dist/__tests__/bootstrap.test.js +76 -0
- package/dist/__tests__/bootstrap.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +84 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/m55.test.d.ts +1 -0
- package/dist/__tests__/m55.test.js +160 -0
- package/dist/__tests__/m55.test.js.map +1 -0
- package/dist/__tests__/middleware.test.d.ts +1 -0
- package/dist/__tests__/middleware.test.js +169 -0
- package/dist/__tests__/middleware.test.js.map +1 -0
- package/dist/__tests__/modelFactory.test.d.ts +1 -0
- package/dist/__tests__/modelFactory.test.js +50 -0
- package/dist/__tests__/modelFactory.test.js.map +1 -0
- package/dist/__tests__/optimizations.test.d.ts +1 -0
- package/dist/__tests__/optimizations.test.js +136 -0
- package/dist/__tests__/optimizations.test.js.map +1 -0
- package/dist/__tests__/promptBuilder.test.d.ts +1 -0
- package/dist/__tests__/promptBuilder.test.js +108 -0
- package/dist/__tests__/promptBuilder.test.js.map +1 -0
- package/dist/__tests__/sandbox.test.d.ts +1 -0
- package/dist/__tests__/sandbox.test.js +78 -0
- package/dist/__tests__/sandbox.test.js.map +1 -0
- package/dist/__tests__/security.test.d.ts +1 -0
- package/dist/__tests__/security.test.js +86 -0
- package/dist/__tests__/security.test.js.map +1 -0
- package/dist/__tests__/streaming.test.d.ts +1 -0
- package/dist/__tests__/streaming.test.js +71 -0
- package/dist/__tests__/streaming.test.js.map +1 -0
- package/dist/__tests__/toolRouter.test.d.ts +1 -0
- package/dist/__tests__/toolRouter.test.js +37 -0
- package/dist/__tests__/toolRouter.test.js.map +1 -0
- package/dist/__tests__/tools.test.d.ts +1 -0
- package/dist/__tests__/tools.test.js +112 -0
- package/dist/__tests__/tools.test.js.map +1 -0
- package/dist/__tests__/tracing.test.d.ts +1 -0
- package/dist/__tests__/tracing.test.js +147 -0
- package/dist/__tests__/tracing.test.js.map +1 -0
- package/dist/cli/config.d.ts +49 -0
- package/dist/cli/config.js +86 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +625 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/modelFactory.d.ts +9 -0
- package/dist/cli/modelFactory.js +154 -0
- package/dist/cli/modelFactory.js.map +1 -0
- package/dist/cli/providers.d.ts +18 -0
- package/dist/cli/providers.js +94 -0
- package/dist/cli/providers.js.map +1 -0
- package/dist/core/agentLoop.d.ts +43 -0
- package/dist/core/agentLoop.js +245 -0
- package/dist/core/agentLoop.js.map +1 -0
- package/dist/core/errors.d.ts +62 -0
- package/dist/core/errors.js +139 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/promptBuilder.d.ts +49 -0
- package/dist/core/promptBuilder.js +84 -0
- package/dist/core/promptBuilder.js.map +1 -0
- package/dist/core/reasoningRouter.d.ts +62 -0
- package/dist/core/reasoningRouter.js +102 -0
- package/dist/core/reasoningRouter.js.map +1 -0
- package/dist/core/retry.d.ts +25 -0
- package/dist/core/retry.js +49 -0
- package/dist/core/retry.js.map +1 -0
- package/dist/core/sessionResumer.d.ts +17 -0
- package/dist/core/sessionResumer.js +78 -0
- package/dist/core/sessionResumer.js.map +1 -0
- package/dist/core/sessionStore.d.ts +45 -0
- package/dist/core/sessionStore.js +167 -0
- package/dist/core/sessionStore.js.map +1 -0
- package/dist/core/tokenCounter.d.ts +17 -0
- package/dist/core/tokenCounter.js +54 -0
- package/dist/core/tokenCounter.js.map +1 -0
- package/dist/evals/dataset.d.ts +4 -0
- package/dist/evals/dataset.js +61 -0
- package/dist/evals/dataset.js.map +1 -0
- package/dist/evals/evaluator.d.ts +21 -0
- package/dist/evals/evaluator.js +68 -0
- package/dist/evals/evaluator.js.map +1 -0
- package/dist/hitl/bridge.d.ts +65 -0
- package/dist/hitl/bridge.js +120 -0
- package/dist/hitl/bridge.js.map +1 -0
- package/dist/middleware/commandSanitizer.d.ts +18 -0
- package/dist/middleware/commandSanitizer.js +50 -0
- package/dist/middleware/commandSanitizer.js.map +1 -0
- package/dist/middleware/loopDetection.d.ts +28 -0
- package/dist/middleware/loopDetection.js +49 -0
- package/dist/middleware/loopDetection.js.map +1 -0
- package/dist/middleware/permission.d.ts +17 -0
- package/dist/middleware/permission.js +59 -0
- package/dist/middleware/permission.js.map +1 -0
- package/dist/middleware/pipeline.d.ts +31 -0
- package/dist/middleware/pipeline.js +62 -0
- package/dist/middleware/pipeline.js.map +1 -0
- package/dist/middleware/preCompletion.d.ts +29 -0
- package/dist/middleware/preCompletion.js +82 -0
- package/dist/middleware/preCompletion.js.map +1 -0
- package/dist/middleware/types.d.ts +40 -0
- package/dist/middleware/types.js +8 -0
- package/dist/middleware/types.js.map +1 -0
- package/dist/sandbox/bootstrap.d.ts +38 -0
- package/dist/sandbox/bootstrap.js +107 -0
- package/dist/sandbox/bootstrap.js.map +1 -0
- package/dist/sandbox/manager.d.ts +72 -0
- package/dist/sandbox/manager.js +180 -0
- package/dist/sandbox/manager.js.map +1 -0
- package/dist/sandbox/sync.d.ts +55 -0
- package/dist/sandbox/sync.js +135 -0
- package/dist/sandbox/sync.js.map +1 -0
- package/dist/skills/loader.d.ts +55 -0
- package/dist/skills/loader.js +132 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/tools.d.ts +5 -0
- package/dist/skills/tools.js +78 -0
- package/dist/skills/tools.js.map +1 -0
- package/dist/skills/types.d.ts +13 -0
- package/dist/skills/types.js +2 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/test_cache.d.ts +1 -0
- package/dist/test_cache.js +55 -0
- package/dist/test_cache.js.map +1 -0
- package/dist/test_google.js +93 -0
- package/dist/tools/askUser.d.ts +10 -0
- package/dist/tools/askUser.js +42 -0
- package/dist/tools/askUser.js.map +1 -0
- package/dist/tools/browser.d.ts +19 -0
- package/dist/tools/browser.js +111 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/index.d.ts +27 -0
- package/dist/tools/index.js +184 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/registry.d.ts +31 -0
- package/dist/tools/registry.js +168 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/router.d.ts +34 -0
- package/dist/tools/router.js +73 -0
- package/dist/tools/router.js.map +1 -0
- package/dist/tools/security.d.ts +28 -0
- package/dist/tools/security.js +183 -0
- package/dist/tools/security.js.map +1 -0
- package/dist/tools/webSearch.d.ts +6 -0
- package/dist/tools/webSearch.js +120 -0
- package/dist/tools/webSearch.js.map +1 -0
- package/dist/tracing/analyzer.d.ts +58 -0
- package/dist/tracing/analyzer.js +190 -0
- package/dist/tracing/analyzer.js.map +1 -0
- package/dist/tracing/langsmith.d.ts +38 -0
- package/dist/tracing/langsmith.js +50 -0
- package/dist/tracing/langsmith.js.map +1 -0
- package/dist/tracing/sessionTracer.d.ts +73 -0
- package/dist/tracing/sessionTracer.js +157 -0
- package/dist/tracing/sessionTracer.js.map +1 -0
- package/dist/tracing/types.d.ts +46 -0
- package/dist/tracing/types.js +5 -0
- package/dist/tracing/types.js.map +1 -0
- package/dist/ui/App.d.ts +24 -0
- package/dist/ui/App.js +172 -0
- package/dist/ui/App.js.map +1 -0
- package/dist/ui/components/HITLPrompt.d.ts +15 -0
- package/dist/ui/components/HITLPrompt.js +35 -0
- package/dist/ui/components/HITLPrompt.js.map +1 -0
- package/dist/ui/components/Header.d.ts +8 -0
- package/dist/ui/components/Header.js +6 -0
- package/dist/ui/components/Header.js.map +1 -0
- package/dist/ui/components/MessageBubble.d.ts +13 -0
- package/dist/ui/components/MessageBubble.js +17 -0
- package/dist/ui/components/MessageBubble.js.map +1 -0
- package/dist/ui/components/StatusBar.d.ts +21 -0
- package/dist/ui/components/StatusBar.js +34 -0
- package/dist/ui/components/StatusBar.js.map +1 -0
- package/dist/ui/components/StreamingText.d.ts +13 -0
- package/dist/ui/components/StreamingText.js +24 -0
- package/dist/ui/components/StreamingText.js.map +1 -0
- package/dist/ui/components/ToolCallPanel.d.ts +15 -0
- package/dist/ui/components/ToolCallPanel.js +18 -0
- package/dist/ui/components/ToolCallPanel.js.map +1 -0
- package/docs/01_insights_and_patterns.md +27 -0
- package/docs/02_edge_cases_and_mitigations.md +143 -0
- package/docs/03_initial_implementation_plan.md +66 -0
- package/docs/04_tech_stack_proposal.md +20 -0
- package/docs/05_prd.md +87 -0
- package/docs/06_user_stories.md +72 -0
- package/docs/07_system_architecture.md +138 -0
- package/docs/08_roadmap.md +200 -0
- package/e2b/Dockerfile +26 -0
- package/package.json +57 -0
- package/src/__tests__/bootstrap.test.ts +111 -0
- package/src/__tests__/config.test.ts +97 -0
- package/src/__tests__/m55.test.ts +238 -0
- package/src/__tests__/middleware.test.ts +219 -0
- package/src/__tests__/modelFactory.test.ts +63 -0
- package/src/__tests__/optimizations.test.ts +201 -0
- package/src/__tests__/promptBuilder.test.ts +141 -0
- package/src/__tests__/sandbox.test.ts +102 -0
- package/src/__tests__/security.test.ts +122 -0
- package/src/__tests__/streaming.test.ts +82 -0
- package/src/__tests__/toolRouter.test.ts +52 -0
- package/src/__tests__/tools.test.ts +146 -0
- package/src/__tests__/tracing.test.ts +196 -0
- package/src/agents/agentRegistry.ts +69 -0
- package/src/agents/agentSpec.ts +67 -0
- package/src/agents/builtinAgents.ts +142 -0
- package/src/cli/config.ts +124 -0
- package/src/cli/index.ts +730 -0
- package/src/cli/modelFactory.ts +174 -0
- package/src/cli/providers.ts +107 -0
- package/src/commands/builtinCommands.ts +293 -0
- package/src/commands/commandRegistry.ts +194 -0
- package/src/core/agentLoop.d.ts.map +1 -0
- package/src/core/agentLoop.ts +312 -0
- package/src/core/autoSave.ts +95 -0
- package/src/core/compactor.ts +252 -0
- package/src/core/contextGuard.ts +129 -0
- package/src/core/errors.ts +202 -0
- package/src/core/promptBuilder.d.ts.map +1 -0
- package/src/core/promptBuilder.ts +139 -0
- package/src/core/reasoningRouter.ts +121 -0
- package/src/core/retry.ts +75 -0
- package/src/core/sessionResumer.ts +90 -0
- package/src/core/sessionStore.ts +215 -0
- package/src/core/subAgent.ts +339 -0
- package/src/core/tokenCounter.ts +64 -0
- package/src/evals/dataset.ts +67 -0
- package/src/evals/evaluator.ts +81 -0
- package/src/hitl/bridge.ts +160 -0
- package/src/middleware/commandSanitizer.ts +60 -0
- package/src/middleware/loopDetection.ts +63 -0
- package/src/middleware/permission.ts +72 -0
- package/src/middleware/pipeline.ts +75 -0
- package/src/middleware/preCompletion.ts +94 -0
- package/src/middleware/types.ts +45 -0
- package/src/sandbox/bootstrap.ts +121 -0
- package/src/sandbox/manager.ts +239 -0
- package/src/sandbox/sync.ts +157 -0
- package/src/skills/loader.ts +143 -0
- package/src/skills/tools.ts +99 -0
- package/src/skills/types.ts +13 -0
- package/src/test_cache.ts +72 -0
- package/src/test_google.js +40 -0
- package/src/test_google.ts +40 -0
- package/src/tools/askUser.ts +47 -0
- package/src/tools/browser.ts +137 -0
- package/src/tools/index.d.ts.map +1 -0
- package/src/tools/index.ts +237 -0
- package/src/tools/registry.ts +198 -0
- package/src/tools/router.ts +78 -0
- package/src/tools/security.ts +220 -0
- package/src/tools/spawnAgent.ts +158 -0
- package/src/tools/webSearch.ts +142 -0
- package/src/tracing/analyzer.ts +265 -0
- package/src/tracing/langsmith.ts +63 -0
- package/src/tracing/sessionTracer.ts +202 -0
- package/src/tracing/types.ts +49 -0
- package/src/types/valyu.d.ts +37 -0
- package/src/ui/App.tsx +404 -0
- package/src/ui/components/HITLPrompt.tsx +119 -0
- package/src/ui/components/Header.tsx +51 -0
- package/src/ui/components/MessageBubble.tsx +46 -0
- package/src/ui/components/StatusBar.tsx +138 -0
- package/src/ui/components/StreamingText.tsx +48 -0
- package/src/ui/components/ToolCallPanel.tsx +80 -0
- package/tests/commands/commands.test.ts +356 -0
- package/tests/core/compactor.test.ts +217 -0
- package/tests/core/retryAndErrors.test.ts +164 -0
- package/tests/core/sessionResumer.test.ts +95 -0
- package/tests/core/sessionStore.test.ts +84 -0
- package/tests/core/stability.test.ts +165 -0
- package/tests/core/subAgent.test.ts +238 -0
- package/tests/hitl/hitlBridge.test.ts +115 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +10 -0
- package/vitest.out +48 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { JooneConfig } from "./config.js";
|
|
5
|
+
import { getProviderDir, PROVIDER_PACKAGE_MAP } from "./providers.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Providers that do NOT require an API key (e.g. local models).
|
|
9
|
+
*/
|
|
10
|
+
const NO_KEY_PROVIDERS = new Set(["ollama"]);
|
|
11
|
+
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Attempts to dynamically import a provider package.
|
|
16
|
+
* First tries the user-local ~/.joone/providers/node_modules directory.
|
|
17
|
+
* If that fails, falls back to a standard require/import for bundled setups.
|
|
18
|
+
*/
|
|
19
|
+
async function loadProviderPackage(provider: string): Promise<any> {
|
|
20
|
+
const packageName = PROVIDER_PACKAGE_MAP[provider];
|
|
21
|
+
if (!packageName) {
|
|
22
|
+
throw new Error(`Unknown package name for provider: ${provider}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 1. Try user-local plugin directory
|
|
26
|
+
const localPluginPath = path.join(getProviderDir(), "node_modules", packageName);
|
|
27
|
+
try {
|
|
28
|
+
// Node.js ESM cannot import absolute directories (ERR_UNSUPPORTED_DIR_IMPORT).
|
|
29
|
+
// We must use createRequire to resolve the actual package.json "main" or "exports" entry point.
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
const resolvedPath = require.resolve(localPluginPath);
|
|
32
|
+
return await import(pathToFileURL(resolvedPath).href);
|
|
33
|
+
} catch (err: any) {
|
|
34
|
+
// Ignore MODULE_NOT_FOUND or similar errors and fallback
|
|
35
|
+
if (err.code !== "ERR_MODULE_NOT_FOUND" && !err.message.includes("Cannot find module")) {
|
|
36
|
+
// console.debug(`Failed to load from plugin dir:`, err.message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Fallback to standard resolution (for npx, bundled versions, etc)
|
|
41
|
+
try {
|
|
42
|
+
return await import(packageName);
|
|
43
|
+
} catch (err: any) {
|
|
44
|
+
throw new Error(`Provider "${provider}" requires the ${packageName} package.\nRun: joone provider add ${provider}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Model Factory
|
|
50
|
+
*
|
|
51
|
+
* Creates a LangChain BaseChatModel based on the JooneConfig.
|
|
52
|
+
* Uses dynamic imports so only the selected provider's package is loaded.
|
|
53
|
+
*/
|
|
54
|
+
export async function createModel(config: JooneConfig): Promise<BaseChatModel> {
|
|
55
|
+
const { provider, model, apiKey, maxTokens, temperature } = config;
|
|
56
|
+
|
|
57
|
+
// API key validation for cloud providers
|
|
58
|
+
if (!NO_KEY_PROVIDERS.has(provider) && !apiKey) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`API key is required for provider "${provider}". Run: joone config`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
switch (provider) {
|
|
66
|
+
case "anthropic": {
|
|
67
|
+
const pkg = await loadProviderPackage(provider);
|
|
68
|
+
const ChatAnthropic = pkg.ChatAnthropic || pkg.default?.ChatAnthropic;
|
|
69
|
+
return new ChatAnthropic({
|
|
70
|
+
modelName: model,
|
|
71
|
+
anthropicApiKey: apiKey,
|
|
72
|
+
maxTokens,
|
|
73
|
+
temperature,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case "openai": {
|
|
78
|
+
const pkg = await loadProviderPackage(provider);
|
|
79
|
+
const ChatOpenAI = pkg.ChatOpenAI || pkg.default?.ChatOpenAI;
|
|
80
|
+
return new ChatOpenAI({
|
|
81
|
+
modelName: model,
|
|
82
|
+
openAIApiKey: apiKey,
|
|
83
|
+
maxTokens,
|
|
84
|
+
temperature,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case "google": {
|
|
89
|
+
const pkg = await loadProviderPackage(provider);
|
|
90
|
+
// Specifically for Google, LangChain frequently uses ChatGoogleGenerativeAI
|
|
91
|
+
const ChatGoogle = pkg.ChatGoogleGenerativeAI || pkg.default?.ChatGoogleGenerativeAI || pkg.ChatGoogleGenAI || pkg.default?.ChatGoogleGenAI;
|
|
92
|
+
|
|
93
|
+
if (!apiKey) {
|
|
94
|
+
throw new Error("API key is required for provider \"google\". Run: joone config");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return new ChatGoogle({
|
|
98
|
+
model: model,
|
|
99
|
+
apiKey: apiKey,
|
|
100
|
+
maxOutputTokens: maxTokens,
|
|
101
|
+
temperature,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case "mistral": {
|
|
106
|
+
const { ChatMistralAI } = await loadProviderPackage(provider);
|
|
107
|
+
return new ChatMistralAI({
|
|
108
|
+
modelName: model,
|
|
109
|
+
apiKey: apiKey,
|
|
110
|
+
maxTokens,
|
|
111
|
+
temperature,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case "groq": {
|
|
116
|
+
const { ChatGroq } = await loadProviderPackage(provider);
|
|
117
|
+
return new ChatGroq({
|
|
118
|
+
modelName: model,
|
|
119
|
+
apiKey: apiKey,
|
|
120
|
+
maxTokens,
|
|
121
|
+
temperature,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case "deepseek": {
|
|
126
|
+
const { ChatDeepSeek } = await loadProviderPackage(provider);
|
|
127
|
+
return new ChatDeepSeek({
|
|
128
|
+
modelName: model,
|
|
129
|
+
apiKey: apiKey,
|
|
130
|
+
maxTokens,
|
|
131
|
+
temperature,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case "fireworks": {
|
|
136
|
+
const { ChatFireworks } = await loadProviderPackage(provider);
|
|
137
|
+
return new ChatFireworks({
|
|
138
|
+
modelName: model,
|
|
139
|
+
fireworksApiKey: apiKey,
|
|
140
|
+
maxTokens,
|
|
141
|
+
temperature,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case "together": {
|
|
146
|
+
const { ChatTogetherAI } = await loadProviderPackage(provider);
|
|
147
|
+
return new ChatTogetherAI({
|
|
148
|
+
modelName: model,
|
|
149
|
+
togetherAIApiKey: apiKey,
|
|
150
|
+
maxTokens,
|
|
151
|
+
temperature,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case "ollama": {
|
|
156
|
+
const { ChatOllama } = await loadProviderPackage(provider);
|
|
157
|
+
return new ChatOllama({
|
|
158
|
+
model: model,
|
|
159
|
+
maxTokens,
|
|
160
|
+
temperature,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
default:
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Unsupported provider: "${provider}". Supported providers: anthropic, openai, google, mistral, groq, deepseek, fireworks, together, ollama.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
} catch (e: unknown) {
|
|
170
|
+
// If LangChain itself throws an API key error (sometimes they do runtime checks)
|
|
171
|
+
if (e instanceof Error && e.message.includes("API key")) throw e;
|
|
172
|
+
throw e; // Rethrow the loadProviderPackage message or other unexpected errors
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import spawn from "cross-spawn";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the absolute path to the user-local providers directory.
|
|
8
|
+
*/
|
|
9
|
+
export function getProviderDir(): string {
|
|
10
|
+
return path.join(os.homedir(), ".joone", "providers");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Maps the internal Joone provider name to the official LangChain NPM package name.
|
|
15
|
+
*/
|
|
16
|
+
export const PROVIDER_PACKAGE_MAP: Record<string, string> = {
|
|
17
|
+
anthropic: "@langchain/anthropic",
|
|
18
|
+
openai: "@langchain/openai",
|
|
19
|
+
google: "@langchain/google-genai",
|
|
20
|
+
mistral: "@langchain/mistralai",
|
|
21
|
+
groq: "@langchain/groq",
|
|
22
|
+
deepseek: "@langchain/deepseek",
|
|
23
|
+
fireworks: "@langchain/community",
|
|
24
|
+
together: "@langchain/community",
|
|
25
|
+
ollama: "@langchain/ollama"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Installs the NPM package for the given provider into the user-local `~/.joone/providers` directory.
|
|
30
|
+
* @param provider The internal Joone provider name (e.g., "google")
|
|
31
|
+
*/
|
|
32
|
+
export async function installProvider(provider: string): Promise<void> {
|
|
33
|
+
const packageName = PROVIDER_PACKAGE_MAP[provider];
|
|
34
|
+
if (!packageName) {
|
|
35
|
+
throw new Error(`Unknown provider: "${provider}"`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const providerDir = getProviderDir();
|
|
39
|
+
|
|
40
|
+
// Ensure the directory exists
|
|
41
|
+
if (!fs.existsSync(providerDir)) {
|
|
42
|
+
fs.mkdirSync(providerDir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
// We use --no-save to avoid creating a package.json and --no-package-lock to avoid lockfiles
|
|
47
|
+
// in this isolated directory, keeping it clean and fast.
|
|
48
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
49
|
+
const args = ["install", packageName, "--prefix", providerDir, "--no-save", "--no-package-lock"];
|
|
50
|
+
|
|
51
|
+
// Using cross-spawn automatically handles Windows command resolution securely without shell:true
|
|
52
|
+
const child = spawn(npmCmd, args, {
|
|
53
|
+
stdio: "ignore", // Suppress NPM's verbose output during install
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
child.on("close", (code) => {
|
|
57
|
+
if (code === 0) {
|
|
58
|
+
resolve();
|
|
59
|
+
} else {
|
|
60
|
+
reject(new Error(`Failed to install ${packageName} (npm install exited with code ${code})`));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
child.on("error", (err) => {
|
|
65
|
+
reject(new Error(`Failed to spawn npm install: ${err.message}`));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Uninstalls the NPM package for the given provider from the user-local `~/.joone/providers` directory.
|
|
72
|
+
* @param provider The internal Joone provider name (e.g., "google")
|
|
73
|
+
*/
|
|
74
|
+
export async function uninstallProvider(provider: string): Promise<void> {
|
|
75
|
+
const packageName = PROVIDER_PACKAGE_MAP[provider];
|
|
76
|
+
if (!packageName) {
|
|
77
|
+
throw new Error(`Unknown provider: "${provider}"`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const providerDir = getProviderDir();
|
|
81
|
+
|
|
82
|
+
// If the directory doesn't exist, it's already uninstalled implicitly
|
|
83
|
+
if (!fs.existsSync(providerDir)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
89
|
+
const args = ["uninstall", packageName, "--prefix", providerDir, "--no-save", "--no-package-lock"];
|
|
90
|
+
|
|
91
|
+
const child = spawn(npmCmd, args, {
|
|
92
|
+
stdio: "ignore",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
child.on("close", (code) => {
|
|
96
|
+
if (code === 0) {
|
|
97
|
+
resolve();
|
|
98
|
+
} else {
|
|
99
|
+
reject(new Error(`Failed to uninstall ${packageName} (npm uninstall exited with code ${code})`));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
child.on("error", (err) => {
|
|
104
|
+
reject(new Error(`Failed to spawn npm uninstall: ${err.message}`));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Slash Commands
|
|
3
|
+
*
|
|
4
|
+
* All default commands that ship with Joone. Each command is a self-contained
|
|
5
|
+
* SlashCommand object registered into the CommandRegistry at session start.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { SlashCommand, CommandContext } from "./commandRegistry.js";
|
|
9
|
+
import { countMessageTokens, estimateTokens } from "../core/tokenCounter.js";
|
|
10
|
+
|
|
11
|
+
// ─── /help ──────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export const HelpCommand: SlashCommand = {
|
|
14
|
+
name: "help",
|
|
15
|
+
aliases: ["h", "?"],
|
|
16
|
+
description: "Show all available commands",
|
|
17
|
+
execute: async (_args, ctx) => {
|
|
18
|
+
// We import the registry's getHelp via context indirectly.
|
|
19
|
+
// The registry is responsible for calling this, so we return a static list.
|
|
20
|
+
const lines = [
|
|
21
|
+
"Available commands:\n",
|
|
22
|
+
" /help (h, ?) — Show this help message",
|
|
23
|
+
" /model (m) [name] — Switch model or show current model",
|
|
24
|
+
" /clear (c) — Clear conversation history",
|
|
25
|
+
" /compact — Manually trigger context compaction",
|
|
26
|
+
" /tokens (t) — Show current token usage",
|
|
27
|
+
" /status (s) — Show session status",
|
|
28
|
+
" /history — Show conversation summary",
|
|
29
|
+
" /undo — Remove last user+agent exchange",
|
|
30
|
+
" /exit (quit, q) — Exit the session",
|
|
31
|
+
];
|
|
32
|
+
return lines.join("\n");
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ─── /model ─────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
export const ModelCommand: SlashCommand = {
|
|
39
|
+
name: "model",
|
|
40
|
+
aliases: ["m"],
|
|
41
|
+
description: "Switch LLM model or show current model",
|
|
42
|
+
execute: async (args, ctx) => {
|
|
43
|
+
if (!args.trim()) {
|
|
44
|
+
return `Current model: ${ctx.model} (provider: ${ctx.provider})\n` +
|
|
45
|
+
`To switch: /model <model-name>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// We can't hot-swap the LLM instance without re-creating the harness,
|
|
49
|
+
// so we update the config and inform the user to restart.
|
|
50
|
+
// In the future, this could support live reloading.
|
|
51
|
+
const newModel = args.trim();
|
|
52
|
+
return `⚠ Model switching requires a session restart.\n` +
|
|
53
|
+
`To use "${newModel}", update your config:\n` +
|
|
54
|
+
` joone config\n` +
|
|
55
|
+
`Or restart with: joone start\n\n` +
|
|
56
|
+
`Current model: ${ctx.model}`;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ─── /clear ─────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export const ClearCommand: SlashCommand = {
|
|
63
|
+
name: "clear",
|
|
64
|
+
aliases: ["c"],
|
|
65
|
+
description: "Clear conversation history (keeps system prompt)",
|
|
66
|
+
execute: async (_args, ctx) => {
|
|
67
|
+
const prevCount = ctx.contextState.conversationHistory.length;
|
|
68
|
+
ctx.setContextState({
|
|
69
|
+
...ctx.contextState,
|
|
70
|
+
conversationHistory: [],
|
|
71
|
+
});
|
|
72
|
+
return `✓ Cleared ${prevCount} messages from conversation history.\n` +
|
|
73
|
+
`System prompt and project context preserved.`;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ─── /compact ───────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
export const CompactCommand: SlashCommand = {
|
|
80
|
+
name: "compact",
|
|
81
|
+
description: "Manually trigger context compaction",
|
|
82
|
+
execute: async (_args, ctx) => {
|
|
83
|
+
const history = ctx.contextState.conversationHistory;
|
|
84
|
+
if (history.length <= 6) {
|
|
85
|
+
return `Not enough history to compact (${history.length} messages). Need > 6.`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const allMessages = [
|
|
89
|
+
...history, // Simplified — the full prompt builder would add system messages too
|
|
90
|
+
];
|
|
91
|
+
const currentTokens = countMessageTokens(allMessages);
|
|
92
|
+
const pct = Math.round((currentTokens / ctx.maxTokens) * 100);
|
|
93
|
+
|
|
94
|
+
if (pct < 50) {
|
|
95
|
+
return `Context at ${pct}% capacity (${currentTokens} tokens). ` +
|
|
96
|
+
`Compaction not needed yet — typically triggers at 80%.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// For now, do a basic compaction. M12 will replace this with LLM-powered compaction.
|
|
100
|
+
const keepLastN = 8;
|
|
101
|
+
const recent = history.slice(-keepLastN);
|
|
102
|
+
const evictedCount = history.length - keepLastN;
|
|
103
|
+
|
|
104
|
+
// Simple string summary (M12 will upgrade to LLM summary)
|
|
105
|
+
const { HumanMessage } = await import("@langchain/core/messages");
|
|
106
|
+
const summaryMsg = new HumanMessage(
|
|
107
|
+
`<system-summary>\n[Compacted: ${evictedCount} messages removed. ` +
|
|
108
|
+
`Use /history for details. Conversation continues below.]\n</system-summary>`
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
ctx.setContextState({
|
|
112
|
+
...ctx.contextState,
|
|
113
|
+
conversationHistory: [summaryMsg, ...recent],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const newTokens = countMessageTokens([summaryMsg, ...recent]);
|
|
117
|
+
return `✓ Compacted ${evictedCount} messages. ` +
|
|
118
|
+
`Tokens: ${currentTokens} → ${newTokens} (${Math.round((newTokens / ctx.maxTokens) * 100)}%)`;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// ─── /tokens ────────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
export const TokensCommand: SlashCommand = {
|
|
125
|
+
name: "tokens",
|
|
126
|
+
aliases: ["t"],
|
|
127
|
+
description: "Show current token usage and context capacity",
|
|
128
|
+
execute: async (_args, ctx) => {
|
|
129
|
+
const history = ctx.contextState.conversationHistory;
|
|
130
|
+
const historyTokens = countMessageTokens(history);
|
|
131
|
+
|
|
132
|
+
const systemTokens =
|
|
133
|
+
estimateTokens(ctx.contextState.globalSystemInstructions) +
|
|
134
|
+
estimateTokens(ctx.contextState.projectMemory) +
|
|
135
|
+
estimateTokens(ctx.contextState.sessionContext);
|
|
136
|
+
|
|
137
|
+
const totalTokens = systemTokens + historyTokens;
|
|
138
|
+
const pct = Math.round((totalTokens / ctx.maxTokens) * 100);
|
|
139
|
+
|
|
140
|
+
const bar = generateBar(pct);
|
|
141
|
+
|
|
142
|
+
return [
|
|
143
|
+
`Token Usage:`,
|
|
144
|
+
` System prompt: ~${systemTokens} tokens`,
|
|
145
|
+
` Conversation: ~${historyTokens} tokens (${history.length} messages)`,
|
|
146
|
+
` Total: ~${totalTokens} / ${ctx.maxTokens} tokens`,
|
|
147
|
+
` Capacity: ${bar} ${pct}%`,
|
|
148
|
+
pct >= 80 ? ` ⚠ Near capacity — consider /compact` : "",
|
|
149
|
+
].filter(Boolean).join("\n");
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Generates a simple text progress bar.
|
|
155
|
+
*/
|
|
156
|
+
function generateBar(pct: number): string {
|
|
157
|
+
const filled = Math.round(pct / 5); // 20 chars total
|
|
158
|
+
const empty = 20 - filled;
|
|
159
|
+
return `[${"█".repeat(filled)}${"░".repeat(Math.max(0, empty))}]`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── /status ────────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
export const StatusCommand: SlashCommand = {
|
|
165
|
+
name: "status",
|
|
166
|
+
aliases: ["s"],
|
|
167
|
+
description: "Show current session status",
|
|
168
|
+
execute: async (_args, ctx) => {
|
|
169
|
+
const history = ctx.contextState.conversationHistory;
|
|
170
|
+
const tokens = countMessageTokens(history);
|
|
171
|
+
const pct = Math.round((tokens / ctx.maxTokens) * 100);
|
|
172
|
+
|
|
173
|
+
return [
|
|
174
|
+
`Session Status:`,
|
|
175
|
+
` Provider: ${ctx.provider}`,
|
|
176
|
+
` Model: ${ctx.model}`,
|
|
177
|
+
` Messages: ${history.length}`,
|
|
178
|
+
` Token Usage: ~${tokens} / ${ctx.maxTokens} (${pct}%)`,
|
|
179
|
+
` CWD: ${process.cwd()}`,
|
|
180
|
+
].join("\n");
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// ─── /history ───────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
export const HistoryCommand: SlashCommand = {
|
|
187
|
+
name: "history",
|
|
188
|
+
description: "Show conversation history summary",
|
|
189
|
+
execute: async (_args, ctx) => {
|
|
190
|
+
const history = ctx.contextState.conversationHistory;
|
|
191
|
+
|
|
192
|
+
if (history.length === 0) {
|
|
193
|
+
return "No conversation history yet.";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const lines: string[] = [`Conversation History (${history.length} messages):\n`];
|
|
197
|
+
|
|
198
|
+
// Show last 20 messages max
|
|
199
|
+
const start = Math.max(0, history.length - 20);
|
|
200
|
+
if (start > 0) {
|
|
201
|
+
lines.push(` ... (${start} earlier messages omitted)\n`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (let i = start; i < history.length; i++) {
|
|
205
|
+
const msg = history[i];
|
|
206
|
+
const role = msg._getType();
|
|
207
|
+
const content = typeof msg.content === "string"
|
|
208
|
+
? msg.content.slice(0, 80)
|
|
209
|
+
: "[complex content]";
|
|
210
|
+
const suffix = typeof msg.content === "string" && msg.content.length > 80 ? "..." : "";
|
|
211
|
+
lines.push(` ${i + 1}. [${role}] ${content}${suffix}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return lines.join("\n");
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// ─── /undo ──────────────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
export const UndoCommand: SlashCommand = {
|
|
221
|
+
name: "undo",
|
|
222
|
+
description: "Remove last user message and agent response",
|
|
223
|
+
execute: async (_args, ctx) => {
|
|
224
|
+
const history = ctx.contextState.conversationHistory;
|
|
225
|
+
|
|
226
|
+
if (history.length === 0) {
|
|
227
|
+
return "Nothing to undo — conversation history is empty.";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Walk backwards to remove the last user→agent exchange.
|
|
231
|
+
// An exchange is: HumanMessage → [AIMessage + ToolMessages...]
|
|
232
|
+
let removeCount = 0;
|
|
233
|
+
let hitHuman = false;
|
|
234
|
+
|
|
235
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
236
|
+
const type = history[i]._getType();
|
|
237
|
+
removeCount++;
|
|
238
|
+
|
|
239
|
+
if (type === "human") {
|
|
240
|
+
hitHuman = true;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!hitHuman) {
|
|
246
|
+
// No HumanMessage found — remove the last message only
|
|
247
|
+
removeCount = 1;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const newHistory = history.slice(0, history.length - removeCount);
|
|
251
|
+
ctx.setContextState({
|
|
252
|
+
...ctx.contextState,
|
|
253
|
+
conversationHistory: newHistory,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return `✓ Removed ${removeCount} message(s). History now has ${newHistory.length} messages.`;
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// ─── /exit ──────────────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
export const ExitCommand: SlashCommand = {
|
|
263
|
+
name: "exit",
|
|
264
|
+
aliases: ["quit", "q"],
|
|
265
|
+
description: "Exit the Joone session",
|
|
266
|
+
execute: async (_args, _ctx) => {
|
|
267
|
+
// The TUI will handle the actual exit. We return a signal message.
|
|
268
|
+
return "__EXIT__";
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// ─── Registration Helper ────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
import { CommandRegistry } from "./commandRegistry.js";
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Creates a CommandRegistry pre-loaded with all built-in commands.
|
|
278
|
+
*/
|
|
279
|
+
export function createDefaultRegistry(): CommandRegistry {
|
|
280
|
+
const registry = new CommandRegistry();
|
|
281
|
+
|
|
282
|
+
registry.register(HelpCommand);
|
|
283
|
+
registry.register(ModelCommand);
|
|
284
|
+
registry.register(ClearCommand);
|
|
285
|
+
registry.register(CompactCommand);
|
|
286
|
+
registry.register(TokensCommand);
|
|
287
|
+
registry.register(StatusCommand);
|
|
288
|
+
registry.register(HistoryCommand);
|
|
289
|
+
registry.register(UndoCommand);
|
|
290
|
+
registry.register(ExitCommand);
|
|
291
|
+
|
|
292
|
+
return registry;
|
|
293
|
+
}
|