@llmtune/cli 0.1.7 → 0.1.9
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/agent/loop.js +23 -6
- package/dist/commands/balance.d.ts +2 -0
- package/dist/commands/balance.js +65 -0
- package/dist/commands/chat.js +15 -11
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +212 -0
- package/dist/commands/login.js +3 -1
- package/dist/commands/models.js +9 -15
- package/dist/commands/update.d.ts +6 -0
- package/dist/commands/update.js +115 -0
- package/dist/context/cache.js +110 -0
- package/dist/context/workspace.js +2 -1
- package/dist/index.js +57 -5
- package/dist/repl/repl.d.ts +1 -0
- package/dist/repl/repl.js +51 -34
- package/dist/tools/permissions.d.ts +2 -0
- package/dist/tools/permissions.js +12 -3
- package/dist/tools/sanitize.d.ts +10 -0
- package/dist/tools/sanitize.js +87 -0
- package/dist/tools/tools/edit.js +8 -2
- package/dist/tools/tools/grep.js +2 -2
- package/dist/tools/tools/read.js +7 -4
- package/dist/tools/tools/web-fetch.js +74 -99
- package/dist/tools/tools/write.js +13 -7
- package/dist/ui/banner.d.ts +2 -0
- package/dist/ui/banner.js +29 -0
- package/dist/utils/streaming.d.ts +6 -3
- package/dist/utils/streaming.js +14 -19
- package/package.json +9 -3
package/dist/context/cache.js
CHANGED
|
@@ -1,8 +1,118 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.getContext = getContext;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
4
41
|
const builder_1 = require("./builder");
|
|
42
|
+
const CACHE_DIR = path.join(os.homedir(), ".llmtune", "cache", "context");
|
|
43
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
44
|
+
function getCachePath(hash) {
|
|
45
|
+
return path.join(CACHE_DIR, `${hash}.json`);
|
|
46
|
+
}
|
|
47
|
+
function computeCacheKey(workspaceRoot, cwd) {
|
|
48
|
+
// Hash based on workspace root, cwd, and relevant file mtimes
|
|
49
|
+
const hasher = crypto.createHash("sha256");
|
|
50
|
+
hasher.update(`root:${workspaceRoot}`);
|
|
51
|
+
hasher.update(`cwd:${cwd}`);
|
|
52
|
+
// Include mtimes of instruction files
|
|
53
|
+
const instructionFiles = ["LLMTUNE.md", "CLAUDE.md", ".llmtune", ".claude"];
|
|
54
|
+
for (const name of instructionFiles) {
|
|
55
|
+
for (const dir of [cwd, workspaceRoot]) {
|
|
56
|
+
const fp = path.join(dir, name);
|
|
57
|
+
try {
|
|
58
|
+
const stat = fs.statSync(fp);
|
|
59
|
+
if (stat.isFile()) {
|
|
60
|
+
hasher.update(`${fp}:${stat.mtimeMs}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { /* skip */ }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return hasher.digest("hex").slice(0, 24);
|
|
67
|
+
}
|
|
5
68
|
async function getContext(workspaceRoot, cwd, options) {
|
|
69
|
+
const useCache = options?.useCache !== false; // default true
|
|
70
|
+
if (useCache) {
|
|
71
|
+
const key = computeCacheKey(workspaceRoot, cwd);
|
|
72
|
+
const cachePath = getCachePath(key);
|
|
73
|
+
try {
|
|
74
|
+
const stat = fs.statSync(cachePath);
|
|
75
|
+
const age = Date.now() - stat.mtimeMs;
|
|
76
|
+
if (age < CACHE_TTL_MS) {
|
|
77
|
+
const cached = JSON.parse(fs.readFileSync(cachePath, "utf-8"));
|
|
78
|
+
return cached;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch { /* cache miss */ }
|
|
82
|
+
// Build fresh context and cache it
|
|
83
|
+
const result = await (0, builder_1.buildContextPrompt)(workspaceRoot, cwd, options);
|
|
84
|
+
try {
|
|
85
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
86
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
fs.writeFileSync(cachePath, JSON.stringify(result), "utf-8");
|
|
89
|
+
// Prune old cache entries (keep last 20)
|
|
90
|
+
pruneCache();
|
|
91
|
+
}
|
|
92
|
+
catch { /* cache write failure is non-critical */ }
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
6
95
|
return (0, builder_1.buildContextPrompt)(workspaceRoot, cwd, options);
|
|
7
96
|
}
|
|
97
|
+
function pruneCache(maxEntries = 20) {
|
|
98
|
+
try {
|
|
99
|
+
const entries = fs.readdirSync(CACHE_DIR)
|
|
100
|
+
.filter(f => f.endsWith(".json"))
|
|
101
|
+
.map(f => ({
|
|
102
|
+
name: f,
|
|
103
|
+
path: path.join(CACHE_DIR, f),
|
|
104
|
+
mtime: fs.statSync(path.join(CACHE_DIR, f)).mtimeMs,
|
|
105
|
+
}))
|
|
106
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
107
|
+
if (entries.length > maxEntries) {
|
|
108
|
+
for (const entry of entries.slice(maxEntries)) {
|
|
109
|
+
try {
|
|
110
|
+
fs.unlinkSync(entry.path);
|
|
111
|
+
}
|
|
112
|
+
catch { /* skip */ }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch { /* skip */ }
|
|
117
|
+
}
|
|
8
118
|
//# sourceMappingURL=cache.js.map
|
|
@@ -97,7 +97,8 @@ async function walkDir(root, relDir, visitor, depth = 0) {
|
|
|
97
97
|
continue;
|
|
98
98
|
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
99
99
|
const isDir = entry.isDirectory();
|
|
100
|
-
|
|
100
|
+
// FIX: was passing relDir instead of relPath — key file detection was broken
|
|
101
|
+
await visitor(entry.name, relPath, isDir);
|
|
101
102
|
if (isDir && depth < 3) {
|
|
102
103
|
await walkDir(root, relPath, visitor, depth + 1);
|
|
103
104
|
}
|
package/dist/index.js
CHANGED
|
@@ -43,11 +43,22 @@ const config_1 = require("./auth/config");
|
|
|
43
43
|
const client_1 = require("./auth/client");
|
|
44
44
|
const repl_1 = require("./repl/repl");
|
|
45
45
|
const version_1 = require("./version");
|
|
46
|
+
const banner_1 = require("./ui/banner");
|
|
46
47
|
const program = new commander_1.Command();
|
|
47
48
|
program
|
|
48
49
|
.name("llmtune")
|
|
49
50
|
.description("AI CLI Agent powered by llmtune.io")
|
|
50
51
|
.version(version_1.CLI_VERSION);
|
|
52
|
+
// Default action: show banner + help when no subcommand
|
|
53
|
+
program.action(() => {
|
|
54
|
+
console.log((0, banner_1.renderBanner)());
|
|
55
|
+
console.log(chalk_1.default.dim(" Quick start:"));
|
|
56
|
+
console.log(chalk_1.default.dim(` ${chalk_1.default.bold("llmtune login")} Configure your API key`));
|
|
57
|
+
console.log(chalk_1.default.dim(` ${chalk_1.default.bold("llmtune chat")} Start an interactive session`));
|
|
58
|
+
console.log(chalk_1.default.dim(` ${chalk_1.default.bold("llmtune doctor")} Diagnose your setup`));
|
|
59
|
+
console.log("");
|
|
60
|
+
program.outputHelp();
|
|
61
|
+
});
|
|
51
62
|
program
|
|
52
63
|
.command("login")
|
|
53
64
|
.description("Configure API key and settings")
|
|
@@ -60,6 +71,7 @@ program
|
|
|
60
71
|
.description("Start interactive coding session")
|
|
61
72
|
.option("-m, --model <model>", "Model to use")
|
|
62
73
|
.option("--no-stream", "Disable streaming")
|
|
74
|
+
.option("--resume", "Resume most recent session")
|
|
63
75
|
.action(async (options) => {
|
|
64
76
|
if (!(0, config_1.isLoggedIn)()) {
|
|
65
77
|
console.log(chalk_1.default.red('Not logged in. Run "llmtune login" first.'));
|
|
@@ -69,11 +81,23 @@ program
|
|
|
69
81
|
const client = (0, client_1.createClient)();
|
|
70
82
|
const model = options.model ?? config.defaultModel ?? "z-ai/GLM-5.1";
|
|
71
83
|
const apiBase = config.apiBase ?? "https://api.llmtune.io/api/agent/v1";
|
|
72
|
-
|
|
73
|
-
console.log(
|
|
74
|
-
|
|
75
|
-
console.log(chalk_1.default.dim(
|
|
76
|
-
|
|
84
|
+
// Render the LLMTune ASCII art banner
|
|
85
|
+
console.log((0, banner_1.renderBanner)());
|
|
86
|
+
// Connection info — only here, not duplicated in startRepl
|
|
87
|
+
console.log(chalk_1.default.dim(` Connected to ${apiBase}`));
|
|
88
|
+
console.log(chalk_1.default.dim(` Model: ${model}`));
|
|
89
|
+
console.log(chalk_1.default.dim(` Type /help for commands, /exit to quit`));
|
|
90
|
+
console.log("");
|
|
91
|
+
// Quietly check for updates on startup
|
|
92
|
+
try {
|
|
93
|
+
const { checkForUpdate } = await Promise.resolve().then(() => __importStar(require("./commands/update")));
|
|
94
|
+
const latest = checkForUpdate();
|
|
95
|
+
if (latest) {
|
|
96
|
+
console.log(chalk_1.default.dim(` 📦 Update available: v${version_1.CLI_VERSION} → v${latest}. Run: llmtune update\n`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch { /* non-critical */ }
|
|
100
|
+
await (0, repl_1.startRepl)({ client, model, stream: options.stream !== false, resume: options.resume });
|
|
77
101
|
});
|
|
78
102
|
program
|
|
79
103
|
.command("models")
|
|
@@ -89,6 +113,34 @@ program
|
|
|
89
113
|
const { showConfig } = await Promise.resolve().then(() => __importStar(require("./commands/config")));
|
|
90
114
|
showConfig();
|
|
91
115
|
});
|
|
116
|
+
program
|
|
117
|
+
.command("balance")
|
|
118
|
+
.description("Show account balance and usage")
|
|
119
|
+
.action(async () => {
|
|
120
|
+
const { balanceCommand } = await Promise.resolve().then(() => __importStar(require("./commands/balance")));
|
|
121
|
+
await balanceCommand();
|
|
122
|
+
});
|
|
123
|
+
program
|
|
124
|
+
.command("doctor")
|
|
125
|
+
.description("Diagnose CLI setup and configuration")
|
|
126
|
+
.action(async () => {
|
|
127
|
+
const { doctorCommand } = await Promise.resolve().then(() => __importStar(require("./commands/doctor")));
|
|
128
|
+
await doctorCommand();
|
|
129
|
+
});
|
|
130
|
+
program
|
|
131
|
+
.command("update")
|
|
132
|
+
.description("Check for and install CLI updates")
|
|
133
|
+
.action(async () => {
|
|
134
|
+
const { updateCommand } = await Promise.resolve().then(() => __importStar(require("./commands/update")));
|
|
135
|
+
await updateCommand();
|
|
136
|
+
});
|
|
137
|
+
program
|
|
138
|
+
.command("logout")
|
|
139
|
+
.description("Remove saved API key")
|
|
140
|
+
.action(() => {
|
|
141
|
+
(0, config_1.logout)();
|
|
142
|
+
console.log(chalk_1.default.green("✓ Logged out successfully."));
|
|
143
|
+
});
|
|
92
144
|
// Skills marketplace commands
|
|
93
145
|
const skillsCmd = program.command("skills").description("Manage skills (list, install, publish, sign)");
|
|
94
146
|
skillsCmd
|
package/dist/repl/repl.d.ts
CHANGED
package/dist/repl/repl.js
CHANGED
|
@@ -60,28 +60,28 @@ const trust_1 = require("../skills/trust");
|
|
|
60
60
|
const service_2 = require("../memory/service");
|
|
61
61
|
const logger_1 = require("../telemetry/logger");
|
|
62
62
|
const config_1 = require("../auth/config");
|
|
63
|
-
const version_1 = require("../version");
|
|
64
63
|
const fs = __importStar(require("fs"));
|
|
65
64
|
const path = __importStar(require("path"));
|
|
66
|
-
const HELP_TEXT = `
|
|
67
|
-
${chalk_1.default.bold("LLMTune CLI - Commands:")}
|
|
68
|
-
|
|
69
|
-
${chalk_1.default.cyan("/help")} Show this help
|
|
70
|
-
${chalk_1.default.cyan("/clear")} Clear conversation history
|
|
71
|
-
${chalk_1.default.cyan("/context")} Show detailed context usage breakdown
|
|
72
|
-
${chalk_1.default.cyan("/compact")} Compact conversation (LLM summary)
|
|
73
|
-
${chalk_1.default.cyan("/uncompact")} Restore conversation from before compaction
|
|
74
|
-
${chalk_1.default.cyan("/model <name>")} Switch model
|
|
75
|
-
${chalk_1.default.cyan("/stream")} Toggle streaming mode
|
|
76
|
-
${chalk_1.default.cyan("/verbose")} Toggle verbose tool output
|
|
77
|
-
${chalk_1.default.cyan("/trust <tool>")} Trust a tool (skip confirmations)
|
|
78
|
-
${chalk_1.default.cyan("/skills")} List available skills
|
|
79
|
-
${chalk_1.default.cyan("/memory")} Show memory contents
|
|
80
|
-
${chalk_1.default.cyan("/
|
|
81
|
-
${chalk_1.default.cyan("/
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
${chalk_1.default.gray("
|
|
65
|
+
const HELP_TEXT = `
|
|
66
|
+
${chalk_1.default.bold("LLMTune CLI - Commands:")}
|
|
67
|
+
|
|
68
|
+
${chalk_1.default.cyan("/help")} Show this help
|
|
69
|
+
${chalk_1.default.cyan("/clear")} Clear conversation history
|
|
70
|
+
${chalk_1.default.cyan("/context")} Show detailed context usage breakdown
|
|
71
|
+
${chalk_1.default.cyan("/compact")} Compact conversation (LLM summary)
|
|
72
|
+
${chalk_1.default.cyan("/uncompact")} Restore conversation from before compaction
|
|
73
|
+
${chalk_1.default.cyan("/model <name>")} Switch model
|
|
74
|
+
${chalk_1.default.cyan("/stream")} Toggle streaming mode
|
|
75
|
+
${chalk_1.default.cyan("/verbose")} Toggle verbose tool output
|
|
76
|
+
${chalk_1.default.cyan("/trust <tool>")} Trust a tool (skip confirmations)
|
|
77
|
+
${chalk_1.default.cyan("/skills")} List available skills
|
|
78
|
+
${chalk_1.default.cyan("/memory")} Show memory contents
|
|
79
|
+
${chalk_1.default.cyan("/balance")} Show account balance
|
|
80
|
+
${chalk_1.default.cyan("/save")} Save conversation to file
|
|
81
|
+
${chalk_1.default.cyan("/exit")} Exit REPL
|
|
82
|
+
|
|
83
|
+
${chalk_1.default.gray("Type your message and press Enter to chat.")}
|
|
84
|
+
${chalk_1.default.gray("Multi-line: end a line with '\\' to continue.")}
|
|
85
85
|
`.trim();
|
|
86
86
|
async function startRepl(options) {
|
|
87
87
|
const registry = new registry_1.ToolRegistry();
|
|
@@ -110,13 +110,12 @@ async function startRepl(options) {
|
|
|
110
110
|
let currentModel = options.model;
|
|
111
111
|
let streamMode = options.stream;
|
|
112
112
|
let verbose = false;
|
|
113
|
-
|
|
114
|
-
console.log(chalk_1.default.dim(`
|
|
115
|
-
console.log(chalk_1.default.dim(`Tools: ${registry.listSpecs().map((s) => s.name).join(", ")}`));
|
|
113
|
+
// Show tools & skills only (banner/version/model already printed by chat command)
|
|
114
|
+
console.log(chalk_1.default.dim(` Tools: ${registry.listSpecs().map((s) => s.name).join(", ")}`));
|
|
116
115
|
if (skillList.length > 0) {
|
|
117
|
-
console.log(chalk_1.default.dim(`Skills: ${skillList.map((s) => s.name).join(", ")}`));
|
|
116
|
+
console.log(chalk_1.default.dim(` Skills: ${skillList.map((s) => s.name).join(", ")}`));
|
|
118
117
|
}
|
|
119
|
-
console.log(
|
|
118
|
+
console.log("");
|
|
120
119
|
const rl = (0, readline_1.createInterface)({
|
|
121
120
|
input: process.stdin,
|
|
122
121
|
output: process.stdout,
|
|
@@ -138,7 +137,7 @@ async function startRepl(options) {
|
|
|
138
137
|
return;
|
|
139
138
|
}
|
|
140
139
|
// Check for skill execution: /skill-name [args]
|
|
141
|
-
if (fullInput.startsWith("/") && !fullInput.startsWith("/help") && !fullInput.startsWith("/exit") && !fullInput.startsWith("/quit") && !fullInput.startsWith("/clear") && !fullInput.startsWith("/context") && !fullInput.startsWith("/compact") && !fullInput.startsWith("/uncompact") && !fullInput.startsWith("/model") && !fullInput.startsWith("/stream") && !fullInput.startsWith("/verbose") && !fullInput.startsWith("/trust") && !fullInput.startsWith("/skills") && !fullInput.startsWith("/memory") && !fullInput.startsWith("/save")) {
|
|
140
|
+
if (fullInput.startsWith("/") && !fullInput.startsWith("/help") && !fullInput.startsWith("/exit") && !fullInput.startsWith("/quit") && !fullInput.startsWith("/clear") && !fullInput.startsWith("/context") && !fullInput.startsWith("/compact") && !fullInput.startsWith("/uncompact") && !fullInput.startsWith("/model") && !fullInput.startsWith("/stream") && !fullInput.startsWith("/verbose") && !fullInput.startsWith("/trust") && !fullInput.startsWith("/skills") && !fullInput.startsWith("/memory") && !fullInput.startsWith("/save") && !fullInput.startsWith("/balance")) {
|
|
142
141
|
const parts = fullInput.slice(1).split(/\s+/);
|
|
143
142
|
const skillName = parts[0];
|
|
144
143
|
const skillArgs = parts.slice(1);
|
|
@@ -203,6 +202,7 @@ async function startRepl(options) {
|
|
|
203
202
|
}
|
|
204
203
|
// Normal chat
|
|
205
204
|
try {
|
|
205
|
+
const startTime = Date.now();
|
|
206
206
|
const result = await (0, loop_1.runAgentLoop)(options.client, conversation, registry, fullInput, {
|
|
207
207
|
model: currentModel,
|
|
208
208
|
maxTurns: 50,
|
|
@@ -214,17 +214,20 @@ async function startRepl(options) {
|
|
|
214
214
|
});
|
|
215
215
|
if (result.totalTokensIn > 0 || result.totalTokensOut > 0) {
|
|
216
216
|
const cost = estimateCostFromUsage(result.totalTokensIn, result.totalTokensOut);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
218
|
+
const costStr = cost < 0.001 ? `<$0.001` : `~$${cost.toFixed(4)}`;
|
|
219
|
+
console.log(chalk_1.default.dim(` [${elapsed}s | ` +
|
|
220
|
+
`${result.turns} turn${result.turns !== 1 ? "s" : ""} | ` +
|
|
221
|
+
`${result.totalToolCalls} tool${result.totalToolCalls !== 1 ? "s" : ""} | ` +
|
|
222
|
+
`${result.totalTokensIn + result.totalTokensOut} tok | ` +
|
|
223
|
+
`${costStr}]`));
|
|
221
224
|
(0, logger_1.logEvent)({
|
|
222
225
|
event: "llm_response",
|
|
223
226
|
model: currentModel,
|
|
224
227
|
tokens_in: result.totalTokensIn,
|
|
225
228
|
tokens_out: result.totalTokensOut,
|
|
226
229
|
cost,
|
|
227
|
-
latency_ms:
|
|
230
|
+
latency_ms: Date.now() - startTime,
|
|
228
231
|
});
|
|
229
232
|
}
|
|
230
233
|
}
|
|
@@ -257,10 +260,12 @@ async function handleCommand(input, ctx) {
|
|
|
257
260
|
case "/q":
|
|
258
261
|
console.log(chalk_1.default.dim("Goodbye!"));
|
|
259
262
|
process.exit(0);
|
|
260
|
-
case "/clear":
|
|
261
|
-
|
|
263
|
+
case "/clear": {
|
|
264
|
+
// Use splice to properly clear messages array
|
|
265
|
+
ctx.conversation.messages.splice(0, ctx.conversation.messages.length);
|
|
262
266
|
console.log(chalk_1.default.green("Conversation cleared."));
|
|
263
267
|
break;
|
|
268
|
+
}
|
|
264
269
|
case "/context": {
|
|
265
270
|
const ctxResult = await (0, builder_1.buildContextPrompt)(ctx.cwd, ctx.cwd, { model: ctx.getModel() });
|
|
266
271
|
const analysis = (0, analyzer_1.analyzeContextUsage)({
|
|
@@ -368,8 +373,20 @@ async function handleCommand(input, ctx) {
|
|
|
368
373
|
}
|
|
369
374
|
break;
|
|
370
375
|
}
|
|
376
|
+
case "/balance": {
|
|
377
|
+
try {
|
|
378
|
+
const { balanceCommand } = await Promise.resolve().then(() => __importStar(require("../commands/balance")));
|
|
379
|
+
await balanceCommand();
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
383
|
+
console.log(chalk_1.default.red(`Failed to fetch balance: ${msg}`));
|
|
384
|
+
}
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
371
387
|
case "/save": {
|
|
372
|
-
const
|
|
388
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
389
|
+
const savePath = path.join(process.cwd(), `llmtune-session-${timestamp}.json`);
|
|
373
390
|
fs.writeFileSync(savePath, JSON.stringify(ctx.conversation.getApiMessages(), null, 2), "utf-8");
|
|
374
391
|
console.log(chalk_1.default.green(`Session saved to: ${savePath}`));
|
|
375
392
|
break;
|
|
@@ -15,6 +15,8 @@ export declare class PermissionManager {
|
|
|
15
15
|
constructor();
|
|
16
16
|
trustTool(toolName: string): void;
|
|
17
17
|
isTrusted(toolName: string): boolean;
|
|
18
|
+
/** Get list of all trusted tool names for this session */
|
|
19
|
+
trustedTools(): string[];
|
|
18
20
|
check(toolName: string, input: Record<string, unknown>, isDestructive: boolean): Promise<PermissionCheckResult>;
|
|
19
21
|
}
|
|
20
22
|
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.PermissionManager = void 0;
|
|
7
|
-
const prompts_1 =
|
|
7
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
8
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
9
|
class PermissionManager {
|
|
10
10
|
config;
|
|
@@ -21,6 +21,15 @@ class PermissionManager {
|
|
|
21
21
|
isTrusted(toolName) {
|
|
22
22
|
return this.config.sessionTrust.get(toolName) === true;
|
|
23
23
|
}
|
|
24
|
+
/** Get list of all trusted tool names for this session */
|
|
25
|
+
trustedTools() {
|
|
26
|
+
const trusted = [];
|
|
27
|
+
for (const [name, isTrusted] of this.config.sessionTrust) {
|
|
28
|
+
if (isTrusted)
|
|
29
|
+
trusted.push(name);
|
|
30
|
+
}
|
|
31
|
+
return trusted;
|
|
32
|
+
}
|
|
24
33
|
async check(toolName, input, isDestructive) {
|
|
25
34
|
if (this.isTrusted(toolName)) {
|
|
26
35
|
return { behavior: "allow" };
|
|
@@ -35,14 +44,14 @@ class PermissionManager {
|
|
|
35
44
|
: JSON.stringify(input).slice(0, 100);
|
|
36
45
|
console.log(chalk_1.default.yellow(`\n⚠ ${toolName} wants to execute:`));
|
|
37
46
|
console.log(chalk_1.default.dim(preview));
|
|
38
|
-
const confirmed = await prompts_1.
|
|
47
|
+
const confirmed = await (0, prompts_1.confirm)({
|
|
39
48
|
message: `Allow ${toolName}? (y/N)`,
|
|
40
49
|
default: false,
|
|
41
50
|
});
|
|
42
51
|
if (!confirmed) {
|
|
43
52
|
return { behavior: "deny", message: "User denied" };
|
|
44
53
|
}
|
|
45
|
-
const alwaysTrust = await prompts_1.
|
|
54
|
+
const alwaysTrust = await (0, prompts_1.confirm)({
|
|
46
55
|
message: `Trust ${toolName} for this session? (y/N)`,
|
|
47
56
|
default: false,
|
|
48
57
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security: validate that a file path is within the workspace root.
|
|
3
|
+
* Prevents path traversal attacks (e.g. ../../etc/passwd).
|
|
4
|
+
*/
|
|
5
|
+
export declare function sanitizeFilePath(filePath: string, workspaceRoot: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Validate a URL is safe for web-fetch (http/https only, no internal IPs by default).
|
|
8
|
+
*/
|
|
9
|
+
export declare function sanitizeUrl(url: string): void;
|
|
10
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.sanitizeFilePath = sanitizeFilePath;
|
|
37
|
+
exports.sanitizeUrl = sanitizeUrl;
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* Security: validate that a file path is within the workspace root.
|
|
41
|
+
* Prevents path traversal attacks (e.g. ../../etc/passwd).
|
|
42
|
+
*/
|
|
43
|
+
function sanitizeFilePath(filePath, workspaceRoot) {
|
|
44
|
+
const resolved = path.resolve(workspaceRoot, filePath);
|
|
45
|
+
// Normalize for comparison (handle .., ., symlinks)
|
|
46
|
+
const normalizedWorkspace = path.normalize(workspaceRoot);
|
|
47
|
+
const normalizedResolved = path.normalize(resolved);
|
|
48
|
+
if (!normalizedResolved.startsWith(normalizedWorkspace + path.sep) &&
|
|
49
|
+
normalizedResolved !== normalizedWorkspace) {
|
|
50
|
+
throw new Error(`Path traversal blocked: "${filePath}" resolves outside workspace root. ` +
|
|
51
|
+
`Resolved: ${normalizedResolved}, Workspace: ${normalizedWorkspace}`);
|
|
52
|
+
}
|
|
53
|
+
return resolved;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Validate a URL is safe for web-fetch (http/https only, no internal IPs by default).
|
|
57
|
+
*/
|
|
58
|
+
function sanitizeUrl(url) {
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = new URL(url);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error(`Invalid URL: ${url}`);
|
|
65
|
+
}
|
|
66
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
67
|
+
throw new Error(`Only http:// and https:// URLs are allowed. Got: ${parsed.protocol}`);
|
|
68
|
+
}
|
|
69
|
+
// Block internal IPs (basic SSRF protection)
|
|
70
|
+
const hostname = parsed.hostname;
|
|
71
|
+
const internalPatterns = [
|
|
72
|
+
/^127\./,
|
|
73
|
+
/^10\./,
|
|
74
|
+
/^172\.(1[6-9]|2\d|3[01])\./,
|
|
75
|
+
/^192\.168\./,
|
|
76
|
+
/^0\./,
|
|
77
|
+
/^localhost$/i,
|
|
78
|
+
/^::1$/,
|
|
79
|
+
/^fd/i,
|
|
80
|
+
];
|
|
81
|
+
for (const pattern of internalPatterns) {
|
|
82
|
+
if (pattern.test(hostname)) {
|
|
83
|
+
throw new Error(`Internal/private network addresses are not allowed: ${hostname}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=sanitize.js.map
|
package/dist/tools/tools/edit.js
CHANGED
|
@@ -35,7 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.editTool = void 0;
|
|
37
37
|
const fs = __importStar(require("fs/promises"));
|
|
38
|
-
const
|
|
38
|
+
const sanitize_1 = require("../sanitize");
|
|
39
39
|
exports.editTool = {
|
|
40
40
|
spec() {
|
|
41
41
|
return {
|
|
@@ -78,7 +78,13 @@ exports.editTool = {
|
|
|
78
78
|
if (!oldString) {
|
|
79
79
|
return { name: "edit", output: { error: "old_string is required" }, isError: true };
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
let resolved;
|
|
82
|
+
try {
|
|
83
|
+
resolved = (0, sanitize_1.sanitizeFilePath)(filePath, ctx.workspaceRoot);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
return { name: "edit", output: { error: err.message }, isError: true };
|
|
87
|
+
}
|
|
82
88
|
let content;
|
|
83
89
|
try {
|
|
84
90
|
content = await fs.readFile(resolved, "utf-8");
|
package/dist/tools/tools/grep.js
CHANGED
|
@@ -86,7 +86,7 @@ exports.grepTool = {
|
|
|
86
86
|
const includeOpt = include ? ` --include="${include}"` : "";
|
|
87
87
|
cmd = `grep -r -n -E${includeOpt} "${pattern.replace(/"/g, "")}" "${searchPath}" 2>/dev/null | head -${maxResults}`;
|
|
88
88
|
}
|
|
89
|
-
const result =
|
|
89
|
+
const result = runGrepExec(cmd, { maxBuffer: 1024 * 1024, timeout: 30000 });
|
|
90
90
|
const output = result.toString().trim();
|
|
91
91
|
const lines = output.split("\n").filter(Boolean);
|
|
92
92
|
const matches = lines.slice(0, maxResults).map((line) => {
|
|
@@ -141,7 +141,7 @@ exports.grepTool = {
|
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
143
|
};
|
|
144
|
-
function
|
|
144
|
+
function runGrepExec(cmd, opts) {
|
|
145
145
|
const { execSync: _execSync } = require("child_process");
|
|
146
146
|
return _execSync(cmd, opts);
|
|
147
147
|
}
|
package/dist/tools/tools/read.js
CHANGED
|
@@ -35,7 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.readTool = void 0;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
|
-
const
|
|
38
|
+
const sanitize_1 = require("../sanitize");
|
|
39
39
|
exports.readTool = {
|
|
40
40
|
spec() {
|
|
41
41
|
return {
|
|
@@ -54,9 +54,12 @@ exports.readTool = {
|
|
|
54
54
|
};
|
|
55
55
|
},
|
|
56
56
|
run(input, ctx) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
let filePath;
|
|
58
|
+
try {
|
|
59
|
+
filePath = (0, sanitize_1.sanitizeFilePath)(String(input.file_path ?? ""), ctx.workspaceRoot);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
return { name: "read", output: { error: err.message }, isError: true };
|
|
60
63
|
}
|
|
61
64
|
if (!fs.existsSync(filePath)) {
|
|
62
65
|
return { name: "read", output: { error: `File not found: ${input.file_path}` }, isError: true };
|