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,174 +0,0 @@
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
- }
@@ -1,28 +0,0 @@
1
- #!/usr/bin/env node
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
-
5
- // We only want to show the banner on post-install if this is an interactive terminal
6
- // and we are not in a CI environment.
7
- if (process.stdout.isTTY && !process.env.CI) {
8
- // Determine if it was installed globally
9
- const npmConfigPrefix = process.env.npm_config_global === "true" || process.env.npm_config_global === "";
10
-
11
- const green = "\x1b[32m";
12
- const cyan = "\x1b[36m";
13
- const bold = "\x1b[1m";
14
- const reset = "\x1b[0m";
15
-
16
- console.log("");
17
- console.log(` ╭───────────────────────────────────────────────────╮`);
18
- console.log(` │ │`);
19
- console.log(` │ 🚀 ${green}${bold}Joone installed successfully!${reset} │`);
20
- console.log(` │ │`);
21
- console.log(` │ To complete setup and start the agent, │`);
22
- console.log(` │ run the following command in your terminal: │`);
23
- console.log(` │ │`);
24
- console.log(` │ $ ${cyan}joone${reset} │`);
25
- console.log(` │ │`);
26
- console.log(` ╰───────────────────────────────────────────────────╯`);
27
- console.log("");
28
- }
@@ -1,107 +0,0 @@
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
- }
@@ -1,293 +0,0 @@
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
- }