@llmtune/cli 0.1.0 → 0.1.2
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/README.md +1 -1
- package/dist/agent/conversation.d.ts +42 -0
- package/dist/agent/conversation.js +105 -0
- package/dist/agent/loop.d.ts +19 -0
- package/dist/agent/loop.js +185 -0
- package/dist/agent/planner.d.ts +8 -0
- package/dist/agent/planner.js +43 -0
- package/dist/auth/client.d.ts +4 -0
- package/dist/auth/client.js +24 -0
- package/dist/auth/config.d.ts +21 -0
- package/dist/auth/config.js +83 -0
- package/dist/commands/chat.d.ts +5 -0
- package/dist/commands/chat.js +27 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +37 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +93 -0
- package/dist/commands/marketplace.d.ts +6 -0
- package/dist/commands/marketplace.js +213 -0
- package/dist/commands/models.d.ts +2 -0
- package/dist/commands/models.js +53 -0
- package/dist/compact/history-store.d.ts +29 -0
- package/dist/compact/history-store.js +110 -0
- package/dist/compact/microcompact.d.ts +10 -0
- package/dist/compact/microcompact.js +43 -0
- package/dist/compact/service.d.ts +13 -0
- package/dist/compact/service.js +156 -0
- package/dist/context/analyzer.d.ts +26 -0
- package/dist/context/analyzer.js +99 -0
- package/dist/context/builder.d.ts +13 -0
- package/dist/context/builder.js +144 -0
- package/dist/context/cache.d.ts +6 -0
- package/dist/context/cache.js +8 -0
- package/dist/context/git-context.d.ts +9 -0
- package/dist/context/git-context.js +39 -0
- package/dist/context/llmtune-md.d.ts +6 -0
- package/dist/context/llmtune-md.js +73 -0
- package/dist/context/workspace.d.ts +11 -0
- package/dist/context/workspace.js +115 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -2
- package/dist/marketplace/client.d.ts +52 -0
- package/dist/marketplace/client.js +86 -0
- package/dist/memory/files.d.ts +14 -0
- package/dist/memory/files.js +116 -0
- package/dist/memory/service.d.ts +22 -0
- package/dist/memory/service.js +146 -0
- package/dist/repl/repl.d.ts +8 -0
- package/dist/repl/repl.js +375 -0
- package/dist/skills/args.d.ts +10 -0
- package/dist/skills/args.js +37 -0
- package/dist/skills/frontmatter.d.ts +6 -0
- package/dist/skills/frontmatter.js +44 -0
- package/dist/skills/loader.d.ts +5 -0
- package/dist/skills/loader.js +59 -0
- package/dist/skills/registry.d.ts +27 -0
- package/dist/skills/registry.js +162 -0
- package/dist/skills/signing/signer.d.ts +19 -0
- package/dist/skills/signing/signer.js +110 -0
- package/dist/skills/trust.d.ts +11 -0
- package/dist/skills/trust.js +42 -0
- package/dist/telemetry/logger.d.ts +51 -0
- package/dist/telemetry/logger.js +135 -0
- package/dist/tools/permissions.d.ts +20 -0
- package/dist/tools/permissions.js +58 -0
- package/dist/tools/protocol.d.ts +22 -0
- package/dist/tools/protocol.js +3 -0
- package/dist/tools/registry.d.ts +20 -0
- package/dist/tools/registry.js +77 -0
- package/dist/tools/sandbox/docker.d.ts +16 -0
- package/dist/tools/sandbox/docker.js +240 -0
- package/dist/tools/sandbox/index.d.ts +18 -0
- package/dist/tools/sandbox/index.js +80 -0
- package/dist/tools/tools/ask-user.d.ts +3 -0
- package/dist/tools/tools/ask-user.js +56 -0
- package/dist/tools/tools/bash.d.ts +3 -0
- package/dist/tools/tools/bash.js +85 -0
- package/dist/tools/tools/edit.d.ts +3 -0
- package/dist/tools/tools/edit.js +138 -0
- package/dist/tools/tools/glob.d.ts +3 -0
- package/dist/tools/tools/glob.js +63 -0
- package/dist/tools/tools/grep.d.ts +3 -0
- package/dist/tools/tools/grep.js +148 -0
- package/dist/tools/tools/read.d.ts +3 -0
- package/dist/tools/tools/read.js +85 -0
- package/dist/tools/tools/web-fetch.d.ts +3 -0
- package/dist/tools/tools/web-fetch.js +143 -0
- package/dist/tools/tools/write.d.ts +3 -0
- package/dist/tools/tools/write.js +84 -0
- package/dist/tools/validation.d.ts +13 -0
- package/dist/tools/validation.js +142 -0
- package/dist/utils/markdown.d.ts +9 -0
- package/dist/utils/markdown.js +89 -0
- package/dist/utils/streaming.d.ts +10 -0
- package/dist/utils/streaming.js +63 -0
- package/dist/utils/tokens.d.ts +12 -0
- package/dist/utils/tokens.js +44 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +9 -0
- package/package.json +2 -2
- package/dist/agent/conversation.d.ts.map +0 -1
- package/dist/agent/loop.d.ts.map +0 -1
- package/dist/agent/planner.d.ts.map +0 -1
- package/dist/auth/client.d.ts.map +0 -1
- package/dist/auth/config.d.ts.map +0 -1
- package/dist/commands/chat.d.ts.map +0 -1
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/marketplace.d.ts.map +0 -1
- package/dist/commands/models.d.ts.map +0 -1
- package/dist/compact/history-store.d.ts.map +0 -1
- package/dist/compact/microcompact.d.ts.map +0 -1
- package/dist/compact/service.d.ts.map +0 -1
- package/dist/context/analyzer.d.ts.map +0 -1
- package/dist/context/builder.d.ts.map +0 -1
- package/dist/context/cache.d.ts.map +0 -1
- package/dist/context/git-context.d.ts.map +0 -1
- package/dist/context/llmtune-md.d.ts.map +0 -1
- package/dist/context/workspace.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/marketplace/client.d.ts.map +0 -1
- package/dist/memory/files.d.ts.map +0 -1
- package/dist/memory/service.d.ts.map +0 -1
- package/dist/repl/repl.d.ts.map +0 -1
- package/dist/skills/args.d.ts.map +0 -1
- package/dist/skills/frontmatter.d.ts.map +0 -1
- package/dist/skills/loader.d.ts.map +0 -1
- package/dist/skills/registry.d.ts.map +0 -1
- package/dist/skills/signing/signer.d.ts.map +0 -1
- package/dist/skills/trust.d.ts.map +0 -1
- package/dist/telemetry/logger.d.ts.map +0 -1
- package/dist/tools/permissions.d.ts.map +0 -1
- package/dist/tools/protocol.d.ts.map +0 -1
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/sandbox/docker.d.ts.map +0 -1
- package/dist/tools/sandbox/index.d.ts.map +0 -1
- package/dist/tools/tools/ask-user.d.ts.map +0 -1
- package/dist/tools/tools/bash.d.ts.map +0 -1
- package/dist/tools/tools/edit.d.ts.map +0 -1
- package/dist/tools/tools/glob.d.ts.map +0 -1
- package/dist/tools/tools/grep.d.ts.map +0 -1
- package/dist/tools/tools/read.d.ts.map +0 -1
- package/dist/tools/tools/web-fetch.d.ts.map +0 -1
- package/dist/tools/tools/write.d.ts.map +0 -1
- package/dist/tools/validation.d.ts.map +0 -1
- package/dist/utils/markdown.d.ts.map +0 -1
- package/dist/utils/streaming.d.ts.map +0 -1
- package/dist/utils/tokens.d.ts.map +0 -1
- package/src/agent/conversation.ts +0 -140
- package/src/agent/loop.ts +0 -215
- package/src/agent/planner.ts +0 -55
- package/src/auth/client.ts +0 -19
- package/src/auth/config.ts +0 -89
- package/src/commands/chat.ts +0 -28
- package/src/commands/config.ts +0 -36
- package/src/commands/login.ts +0 -63
- package/src/commands/marketplace.ts +0 -190
- package/src/commands/models.ts +0 -74
- package/src/compact/history-store.ts +0 -101
- package/src/compact/microcompact.ts +0 -49
- package/src/compact/service.ts +0 -154
- package/src/context/analyzer.ts +0 -127
- package/src/context/builder.ts +0 -123
- package/src/context/cache.ts +0 -11
- package/src/context/git-context.ts +0 -58
- package/src/context/llmtune-md.ts +0 -48
- package/src/context/workspace.ts +0 -139
- package/src/index.ts +0 -100
- package/src/marketplace/client.ts +0 -118
- package/src/memory/files.ts +0 -81
- package/src/memory/service.ts +0 -124
- package/src/repl/repl.ts +0 -400
- package/src/skills/args.ts +0 -35
- package/src/skills/builtin/explain-code/SKILL.md +0 -30
- package/src/skills/frontmatter.ts +0 -47
- package/src/skills/loader.ts +0 -25
- package/src/skills/registry.ts +0 -155
- package/src/skills/signing/signer.ts +0 -101
- package/src/skills/trust.ts +0 -50
- package/src/telemetry/logger.ts +0 -108
- package/src/tools/permissions.ts +0 -83
- package/src/tools/protocol.ts +0 -24
- package/src/tools/registry.ts +0 -93
- package/src/tools/sandbox/docker.ts +0 -225
- package/src/tools/sandbox/index.ts +0 -91
- package/src/tools/tools/ask-user.ts +0 -60
- package/src/tools/tools/bash.ts +0 -97
- package/src/tools/tools/edit.ts +0 -111
- package/src/tools/tools/glob.ts +0 -68
- package/src/tools/tools/grep.ts +0 -121
- package/src/tools/tools/read.ts +0 -57
- package/src/tools/tools/web-fetch.ts +0 -158
- package/src/tools/tools/write.ts +0 -52
- package/src/tools/validation.ts +0 -164
- package/src/utils/markdown.ts +0 -96
- package/src/utils/streaming.ts +0 -63
- package/src/utils/tokens.ts +0 -41
- package/tsconfig.json +0 -20
package/src/repl/repl.ts
DELETED
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk"
|
|
2
|
-
import { createInterface } from "readline"
|
|
3
|
-
import OpenAI from "openai"
|
|
4
|
-
import { Conversation } from "../agent/conversation"
|
|
5
|
-
import { ToolRegistry } from "../tools/registry"
|
|
6
|
-
import { runAgentLoop } from "../agent/loop"
|
|
7
|
-
import { estimateTokens } from "../utils/tokens"
|
|
8
|
-
import { readTool } from "../tools/tools/read"
|
|
9
|
-
import { writeTool } from "../tools/tools/write"
|
|
10
|
-
import { editTool } from "../tools/tools/edit"
|
|
11
|
-
import { bashTool } from "../tools/tools/bash"
|
|
12
|
-
import { globTool } from "../tools/tools/glob"
|
|
13
|
-
import { grepTool } from "../tools/tools/grep"
|
|
14
|
-
import { webFetchTool } from "../tools/tools/web-fetch"
|
|
15
|
-
import { askUserTool } from "../tools/tools/ask-user"
|
|
16
|
-
import { compactConversation, uncompactConversation, type CompactResult } from "../compact/service"
|
|
17
|
-
import { analyzeContextUsage, formatContextAnalysis } from "../context/analyzer"
|
|
18
|
-
import { SkillRegistry, type TrustLevel } from "../skills/registry"
|
|
19
|
-
import { loadAllSkills } from "../skills/loader"
|
|
20
|
-
import { isToolAllowedForSkill, resolveTrustLevel } from "../skills/trust"
|
|
21
|
-
import { initMemoryFiles, buildMemoryPrompt, readAllMemory, type MemoryEntry } from "../memory/service"
|
|
22
|
-
import { startSessionLog, logEvent, endSessionLog, type TelemetryEvent } from "../telemetry/logger"
|
|
23
|
-
import { loadConfig } from "../auth/config"
|
|
24
|
-
import { getConfigDir } from "../auth/config"
|
|
25
|
-
import * as fs from "fs"
|
|
26
|
-
import * as path from "path"
|
|
27
|
-
|
|
28
|
-
export interface ReplOptions {
|
|
29
|
-
client: OpenAI
|
|
30
|
-
model: string
|
|
31
|
-
stream: boolean
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const HELP_TEXT = `
|
|
35
|
-
${chalk.bold("LLMTune CLI - Commands:")}
|
|
36
|
-
|
|
37
|
-
${chalk.cyan("/help")} Show this help
|
|
38
|
-
${chalk.cyan("/clear")} Clear conversation history
|
|
39
|
-
${chalk.cyan("/context")} Show detailed context usage breakdown
|
|
40
|
-
${chalk.cyan("/compact")} Compact conversation (LLM summary)
|
|
41
|
-
${chalk.cyan("/uncompact")} Restore conversation from before compaction
|
|
42
|
-
${chalk.cyan("/model <name>")} Switch model
|
|
43
|
-
${chalk.cyan("/stream")} Toggle streaming mode
|
|
44
|
-
${chalk.cyan("/verbose")} Toggle verbose tool output
|
|
45
|
-
${chalk.cyan("/trust <tool>")} Trust a tool (skip confirmations)
|
|
46
|
-
${chalk.cyan("/skills")} List available skills
|
|
47
|
-
${chalk.cyan("/memory")} Show memory contents
|
|
48
|
-
${chalk.cyan("/save")} Save conversation to file
|
|
49
|
-
${chalk.cyan("/exit")} Exit REPL
|
|
50
|
-
|
|
51
|
-
${chalk.gray("Type your message and press Enter to chat.")}
|
|
52
|
-
${chalk.gray("Multi-line: end a line with '\\' to continue.")}
|
|
53
|
-
`.trim()
|
|
54
|
-
|
|
55
|
-
export async function startRepl(options: ReplOptions): Promise<void> {
|
|
56
|
-
const registry = new ToolRegistry()
|
|
57
|
-
const cwd = process.cwd()
|
|
58
|
-
const trustedTools = new Set<string>()
|
|
59
|
-
|
|
60
|
-
registry.register(readTool)
|
|
61
|
-
registry.register(writeTool)
|
|
62
|
-
registry.register(editTool)
|
|
63
|
-
registry.register(bashTool)
|
|
64
|
-
registry.register(globTool)
|
|
65
|
-
registry.register(grepTool)
|
|
66
|
-
registry.register(webFetchTool)
|
|
67
|
-
registry.register(askUserTool)
|
|
68
|
-
|
|
69
|
-
const conversation = new Conversation(options.model)
|
|
70
|
-
|
|
71
|
-
// Load skills
|
|
72
|
-
const skills = loadAllSkills(cwd)
|
|
73
|
-
const skillList = skills.listUserInvocable()
|
|
74
|
-
|
|
75
|
-
// Initialize memory
|
|
76
|
-
initMemoryFiles()
|
|
77
|
-
|
|
78
|
-
// Start telemetry
|
|
79
|
-
startSessionLog(conversation.id, {
|
|
80
|
-
model: options.model,
|
|
81
|
-
tools: registry.listSpecs().map((s) => s.name),
|
|
82
|
-
cwd,
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
let currentModel = options.model
|
|
86
|
-
let streamMode = options.stream
|
|
87
|
-
let verbose = false
|
|
88
|
-
|
|
89
|
-
console.log(chalk.cyan(`\nLLMTune CLI v0.1.0`))
|
|
90
|
-
console.log(chalk.dim(`Model: ${currentModel}`))
|
|
91
|
-
console.log(chalk.dim(`Tools: ${registry.listSpecs().map((s) => s.name).join(", ")}`))
|
|
92
|
-
if (skillList.length > 0) {
|
|
93
|
-
console.log(chalk.dim(`Skills: ${skillList.map((s) => s.name).join(", ")}`))
|
|
94
|
-
}
|
|
95
|
-
console.log(chalk.dim(`Type /help for commands, /exit to quit.\n`))
|
|
96
|
-
|
|
97
|
-
const rl = createInterface({
|
|
98
|
-
input: process.stdin,
|
|
99
|
-
output: process.stdout,
|
|
100
|
-
prompt: chalk.blue("> "),
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
rl.prompt()
|
|
104
|
-
|
|
105
|
-
let multiLineBuffer = ""
|
|
106
|
-
|
|
107
|
-
rl.on("line", async (line: string) => {
|
|
108
|
-
const trimmed = line.trim()
|
|
109
|
-
|
|
110
|
-
if (trimmed.endsWith("\\")) {
|
|
111
|
-
multiLineBuffer += trimmed.slice(0, -1) + "\n"
|
|
112
|
-
process.stdout.write(chalk.dim("... "))
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const fullInput = multiLineBuffer + trimmed
|
|
117
|
-
multiLineBuffer = ""
|
|
118
|
-
|
|
119
|
-
if (!fullInput) {
|
|
120
|
-
rl.prompt()
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Check for skill execution: /skill-name [args]
|
|
125
|
-
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")) {
|
|
126
|
-
const parts = fullInput.slice(1).split(/\s+/)
|
|
127
|
-
const skillName = parts[0]
|
|
128
|
-
const skillArgs = parts.slice(1)
|
|
129
|
-
|
|
130
|
-
const execution = skills.prepareExecution(skillName, skillArgs)
|
|
131
|
-
if (execution) {
|
|
132
|
-
const skill = skills.get(skillName)!
|
|
133
|
-
console.log(chalk.magenta(`Executing skill: ${skill.name}`))
|
|
134
|
-
|
|
135
|
-
const trustLevel = resolveTrustLevel(skill)
|
|
136
|
-
const toolNames = registry.listSpecs().map((s) => s.name)
|
|
137
|
-
for (const toolName of toolNames) {
|
|
138
|
-
if (!isToolAllowedForSkill(toolName, trustLevel)) {
|
|
139
|
-
console.log(chalk.yellow(` Warning: tool "${toolName}" not allowed for skill trust level "${trustLevel}"`))
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Add skill system prompt and user message
|
|
144
|
-
conversation.addSystemMessage(execution.systemPrompt)
|
|
145
|
-
conversation.addUserMessage(execution.userMessage)
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
const result = await runAgentLoop(options.client, conversation, registry, execution.userMessage, {
|
|
149
|
-
model: currentModel,
|
|
150
|
-
maxTurns: 50,
|
|
151
|
-
verbose,
|
|
152
|
-
cwd,
|
|
153
|
-
workspaceRoot: cwd,
|
|
154
|
-
})
|
|
155
|
-
logEvent({ event: "tool_call", tool: skillName, latency_ms: 0 })
|
|
156
|
-
} catch (err: unknown) {
|
|
157
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
158
|
-
console.log(chalk.red(`\nSkill error: ${msg}`))
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
console.log("")
|
|
162
|
-
rl.prompt()
|
|
163
|
-
return
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (fullInput.startsWith("/")) {
|
|
168
|
-
await handleCommand(fullInput, {
|
|
169
|
-
rl,
|
|
170
|
-
conversation,
|
|
171
|
-
registry,
|
|
172
|
-
skills,
|
|
173
|
-
trustedTools,
|
|
174
|
-
cwd,
|
|
175
|
-
client: options.client,
|
|
176
|
-
getModel: () => currentModel,
|
|
177
|
-
setModel: (m: string) => {
|
|
178
|
-
currentModel = m
|
|
179
|
-
},
|
|
180
|
-
getStream: () => streamMode,
|
|
181
|
-
setStream: (s: boolean) => {
|
|
182
|
-
streamMode = s
|
|
183
|
-
},
|
|
184
|
-
getVerbose: () => verbose,
|
|
185
|
-
setVerbose: (v: boolean) => {
|
|
186
|
-
verbose = v
|
|
187
|
-
},
|
|
188
|
-
})
|
|
189
|
-
rl.prompt()
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Normal chat
|
|
194
|
-
try {
|
|
195
|
-
const result = await runAgentLoop(options.client, conversation, registry, fullInput, {
|
|
196
|
-
model: currentModel,
|
|
197
|
-
maxTurns: 50,
|
|
198
|
-
verbose,
|
|
199
|
-
cwd,
|
|
200
|
-
workspaceRoot: cwd,
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
if (result.totalTokensIn > 0 || result.totalTokensOut > 0) {
|
|
204
|
-
const cost = estimateCostFromUsage(result.totalTokensIn, result.totalTokensOut)
|
|
205
|
-
console.log(
|
|
206
|
-
chalk.dim(
|
|
207
|
-
` [${result.turns} turn${result.turns !== 1 ? "s" : ""} | ` +
|
|
208
|
-
`${result.totalToolCalls} tool calls | ` +
|
|
209
|
-
`${result.totalTokensIn + result.totalTokensOut} tokens | ` +
|
|
210
|
-
`~$${cost.toFixed(4)}]`,
|
|
211
|
-
),
|
|
212
|
-
)
|
|
213
|
-
logEvent({
|
|
214
|
-
event: "llm_response",
|
|
215
|
-
model: currentModel,
|
|
216
|
-
tokens_in: result.totalTokensIn,
|
|
217
|
-
tokens_out: result.totalTokensOut,
|
|
218
|
-
cost,
|
|
219
|
-
latency_ms: 0,
|
|
220
|
-
})
|
|
221
|
-
}
|
|
222
|
-
} catch (err: unknown) {
|
|
223
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
224
|
-
console.log(chalk.red(`\nError: ${msg}`))
|
|
225
|
-
logEvent({ event: "error", source: "agent_loop", error: msg } as TelemetryEvent)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
console.log("")
|
|
229
|
-
rl.prompt()
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
rl.on("close", () => {
|
|
233
|
-
endSessionLog({ totalToolCalls: 0, totalTokens: 0 })
|
|
234
|
-
console.log(chalk.dim("\nGoodbye!"))
|
|
235
|
-
process.exit(0)
|
|
236
|
-
})
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
interface CommandContext {
|
|
240
|
-
rl: ReturnType<typeof createInterface>
|
|
241
|
-
conversation: Conversation
|
|
242
|
-
registry: ToolRegistry
|
|
243
|
-
skills: SkillRegistry
|
|
244
|
-
trustedTools: Set<string>
|
|
245
|
-
cwd: string
|
|
246
|
-
client: OpenAI
|
|
247
|
-
getModel: () => string
|
|
248
|
-
setModel: (m: string) => void
|
|
249
|
-
getStream: () => boolean
|
|
250
|
-
setStream: (s: boolean) => void
|
|
251
|
-
getVerbose: () => boolean
|
|
252
|
-
setVerbose: (v: boolean) => void
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async function handleCommand(input: string, ctx: CommandContext): Promise<void> {
|
|
256
|
-
const parts = input.split(/\s+/)
|
|
257
|
-
const cmd = parts[0].toLowerCase()
|
|
258
|
-
const args = parts.slice(1)
|
|
259
|
-
|
|
260
|
-
switch (cmd) {
|
|
261
|
-
case "/help":
|
|
262
|
-
case "/h":
|
|
263
|
-
case "/?":
|
|
264
|
-
console.log(HELP_TEXT)
|
|
265
|
-
break
|
|
266
|
-
|
|
267
|
-
case "/exit":
|
|
268
|
-
case "/quit":
|
|
269
|
-
case "/q":
|
|
270
|
-
console.log(chalk.dim("Goodbye!"))
|
|
271
|
-
process.exit(0)
|
|
272
|
-
|
|
273
|
-
case "/clear":
|
|
274
|
-
ctx.conversation.messages.length = 0
|
|
275
|
-
console.log(chalk.green("Conversation cleared."))
|
|
276
|
-
break
|
|
277
|
-
|
|
278
|
-
case "/context": {
|
|
279
|
-
const analysis = analyzeContextUsage({
|
|
280
|
-
systemPrompt: "LLMTune coding agent",
|
|
281
|
-
toolSpecs: ctx.registry.listSpecs(),
|
|
282
|
-
messages: ctx.conversation.getApiMessages().map((m) => ({
|
|
283
|
-
role: m.role,
|
|
284
|
-
content: m.content,
|
|
285
|
-
})),
|
|
286
|
-
skillsContent: ctx.skills.list().map((s) => s.content).join("\n") || undefined,
|
|
287
|
-
memoryContent: buildMemoryPrompt() || undefined,
|
|
288
|
-
model: ctx.getModel(),
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
console.log(formatContextAnalysis(analysis))
|
|
292
|
-
break
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
case "/compact": {
|
|
296
|
-
if (ctx.conversation.messages.length < 3) {
|
|
297
|
-
console.log(chalk.yellow("Not enough messages to compact (need at least 3)."))
|
|
298
|
-
break
|
|
299
|
-
}
|
|
300
|
-
console.log(chalk.dim("Compacting conversation..."))
|
|
301
|
-
try {
|
|
302
|
-
const result = await compactConversation(ctx.client, ctx.getModel(), ctx.conversation)
|
|
303
|
-
console.log(chalk.green(`\nCompacted: ${result.preCompactMessages} -> ${result.postCompactMessages} messages`))
|
|
304
|
-
console.log(chalk.dim(` Tokens saved: ~${result.tokensSaved.toLocaleString()}`))
|
|
305
|
-
console.log(chalk.dim(` Use /uncompact to restore full history.`))
|
|
306
|
-
logEvent({ event: "compaction", tokens_saved: result.tokensSaved, messages_before: result.preCompactMessages, messages_after: result.postCompactMessages, trigger: "manual" })
|
|
307
|
-
} catch (err: unknown) {
|
|
308
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
309
|
-
console.log(chalk.red(`Compaction failed: ${msg}`))
|
|
310
|
-
}
|
|
311
|
-
break
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
case "/uncompact": {
|
|
315
|
-
const restored = uncompactConversation(ctx.conversation)
|
|
316
|
-
if (restored) {
|
|
317
|
-
console.log(chalk.green(`Restored conversation from before compaction (${ctx.conversation.messages.length} messages).`))
|
|
318
|
-
} else {
|
|
319
|
-
console.log(chalk.yellow("No raw history found to restore."))
|
|
320
|
-
}
|
|
321
|
-
break
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
case "/model":
|
|
325
|
-
if (args[0]) {
|
|
326
|
-
ctx.setModel(args[0])
|
|
327
|
-
console.log(chalk.green(`Model set to: ${args[0]}`))
|
|
328
|
-
} else {
|
|
329
|
-
console.log(chalk.dim(`Current model: ${ctx.getModel()}`))
|
|
330
|
-
}
|
|
331
|
-
break
|
|
332
|
-
|
|
333
|
-
case "/stream":
|
|
334
|
-
ctx.setStream(!ctx.getStream())
|
|
335
|
-
console.log(chalk.green(`Streaming: ${ctx.getStream() ? "on" : "off"}`))
|
|
336
|
-
break
|
|
337
|
-
|
|
338
|
-
case "/verbose":
|
|
339
|
-
ctx.setVerbose(!ctx.getVerbose())
|
|
340
|
-
console.log(chalk.green(`Verbose: ${ctx.getVerbose() ? "on" : "off"}`))
|
|
341
|
-
break
|
|
342
|
-
|
|
343
|
-
case "/trust":
|
|
344
|
-
if (args[0]) {
|
|
345
|
-
ctx.trustedTools.add(args[0].toLowerCase())
|
|
346
|
-
console.log(chalk.green(`Trusting tool: ${args[0]} (no confirmation needed)`))
|
|
347
|
-
} else {
|
|
348
|
-
console.log(chalk.dim("Usage: /trust <tool-name>"))
|
|
349
|
-
}
|
|
350
|
-
break
|
|
351
|
-
|
|
352
|
-
case "/skills": {
|
|
353
|
-
const allSkills = ctx.skills.listUserInvocable()
|
|
354
|
-
if (allSkills.length === 0) {
|
|
355
|
-
console.log(chalk.dim("No skills loaded. Create skills in ~/.llmtune/skills/ or .llmtune/skills/"))
|
|
356
|
-
console.log(chalk.dim("Each skill is a directory with a SKILL.md file."))
|
|
357
|
-
} else {
|
|
358
|
-
console.log(chalk.bold("\nAvailable Skills:"))
|
|
359
|
-
for (const skill of allSkills) {
|
|
360
|
-
const trustLabel = skill.trustLevel
|
|
361
|
-
console.log(` ${chalk.cyan(`/${skill.name}`)} - ${skill.description} ${chalk.dim(`[${trustLabel}]`)}`)
|
|
362
|
-
}
|
|
363
|
-
console.log("")
|
|
364
|
-
}
|
|
365
|
-
break
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
case "/memory": {
|
|
369
|
-
const entries = readAllMemory()
|
|
370
|
-
if (entries.length === 0) {
|
|
371
|
-
console.log(chalk.dim("\nNo memory entries. The agent will auto-populate memory as you chat."))
|
|
372
|
-
console.log(chalk.dim(`Memory directory: ${getConfigDir()}/memory/\n`))
|
|
373
|
-
} else {
|
|
374
|
-
console.log(chalk.bold("\nMemory:"))
|
|
375
|
-
for (const entry of entries) {
|
|
376
|
-
if (entry.content.trim()) {
|
|
377
|
-
const preview = entry.content.slice(0, 100).replace(/\n/g, " ")
|
|
378
|
-
console.log(` ${chalk.cyan(entry.category)}: ${chalk.dim(preview)}${entry.content.length > 100 ? "..." : ""}`)
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
console.log("")
|
|
382
|
-
}
|
|
383
|
-
break
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
case "/save": {
|
|
387
|
-
const savePath = path.join(process.cwd(), `llmtune-session-${Date.now()}.json`)
|
|
388
|
-
fs.writeFileSync(savePath, JSON.stringify(ctx.conversation.getApiMessages(), null, 2), "utf-8")
|
|
389
|
-
console.log(chalk.green(`Session saved to: ${savePath}`))
|
|
390
|
-
break
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
default:
|
|
394
|
-
console.log(chalk.yellow(`Unknown command: ${cmd}. Type /help for available commands.`))
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function estimateCostFromUsage(inputTokens: number, outputTokens: number): number {
|
|
399
|
-
return (inputTokens * 3 + outputTokens * 15) / 1_000_000
|
|
400
|
-
}
|
package/src/skills/args.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parse argument names from a skill's frontmatter "arguments" field.
|
|
3
|
-
* Supports: "arg1 arg2", "arg1, arg2", "$arg1 $arg2"
|
|
4
|
-
*/
|
|
5
|
-
export function parseArgumentNames(raw: unknown): string[] {
|
|
6
|
-
if (!raw) return []
|
|
7
|
-
const str = String(raw).trim()
|
|
8
|
-
if (!str) return []
|
|
9
|
-
|
|
10
|
-
// Split by comma or space, strip $ prefixes
|
|
11
|
-
return str
|
|
12
|
-
.split(/[,\s]+/)
|
|
13
|
-
.map((s) => s.trim().replace(/^\$/, ""))
|
|
14
|
-
.filter(Boolean)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Substitute {{arg}} placeholders in skill content with provided values.
|
|
19
|
-
*/
|
|
20
|
-
export function substituteArguments(
|
|
21
|
-
content: string,
|
|
22
|
-
args: Record<string, string>
|
|
23
|
-
): string {
|
|
24
|
-
let result = content
|
|
25
|
-
for (const [key, value] of Object.entries(args)) {
|
|
26
|
-
const patterns = [
|
|
27
|
-
new RegExp(`\\{\\{${key}\\}\\}`, "g"),
|
|
28
|
-
new RegExp(`\\$\\{${key}\\}`, "g"),
|
|
29
|
-
]
|
|
30
|
-
for (const pattern of patterns) {
|
|
31
|
-
result = result.replace(pattern, value)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return result
|
|
35
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Explain a code file or code snippet with clear analysis"
|
|
3
|
-
user-invocable: true
|
|
4
|
-
when_to_use: "Use when the user asks to explain, analyze, or understand code"
|
|
5
|
-
allowed-tools:
|
|
6
|
-
- read
|
|
7
|
-
- glob
|
|
8
|
-
- grep
|
|
9
|
-
arguments:
|
|
10
|
-
- file_path
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
You are a code analysis expert. Your task is to explain the given code clearly and thoroughly.
|
|
14
|
-
|
|
15
|
-
## Instructions
|
|
16
|
-
|
|
17
|
-
1. If a `file_path` argument is provided, read the file first using the `read` tool
|
|
18
|
-
2. Provide a structured explanation covering:
|
|
19
|
-
- **Purpose**: What does this code do?
|
|
20
|
-
- **Key Components**: Main functions, classes, or modules
|
|
21
|
-
- **Data Flow**: How data moves through the code
|
|
22
|
-
- **Dependencies**: What external modules or services are used
|
|
23
|
-
- **Notable Patterns**: Design patterns, architectural decisions
|
|
24
|
-
3. Use clear, concise language
|
|
25
|
-
4. Include relevant code snippets in your explanation
|
|
26
|
-
5. If the code has potential issues or improvements, mention them
|
|
27
|
-
|
|
28
|
-
## Output Format
|
|
29
|
-
|
|
30
|
-
Provide the explanation in markdown format with proper headings and code blocks.
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
export interface FrontmatterResult {
|
|
2
|
-
frontmatter: Record<string, unknown>
|
|
3
|
-
body: string
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function parseFrontmatter(content: string): FrontmatterResult {
|
|
7
|
-
const trimmed = content.trimStart()
|
|
8
|
-
|
|
9
|
-
if (!trimmed.startsWith("---")) {
|
|
10
|
-
return { frontmatter: {}, body: content }
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const endMatch = trimmed.indexOf("---", 3)
|
|
14
|
-
if (endMatch === -1 || endMatch < 4) {
|
|
15
|
-
return { frontmatter: {}, body: content }
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const frontmatterStr = trimmed.slice(3, endMatch).trim()
|
|
19
|
-
const body = trimmed.slice(endMatch + 3).trimStart()
|
|
20
|
-
|
|
21
|
-
const frontmatter: Record<string, unknown> = {}
|
|
22
|
-
for (const line of frontmatterStr.split("\n")) {
|
|
23
|
-
const colonIdx = line.indexOf(":")
|
|
24
|
-
if (colonIdx === -1) continue
|
|
25
|
-
const key = line.slice(0, colonIdx).trim()
|
|
26
|
-
let value: unknown = line.slice(colonIdx + 1).trim()
|
|
27
|
-
|
|
28
|
-
if (typeof value === "string") {
|
|
29
|
-
if (value === "true") value = true
|
|
30
|
-
else if (value === "false") value = false
|
|
31
|
-
else if (/^\d+$/.test(value)) value = Number(value)
|
|
32
|
-
else if (value.startsWith("[") && value.endsWith("]")) {
|
|
33
|
-
value = value
|
|
34
|
-
.slice(1, -1)
|
|
35
|
-
.split(",")
|
|
36
|
-
.map((s) => s.trim())
|
|
37
|
-
.filter(Boolean)
|
|
38
|
-
} else {
|
|
39
|
-
value = value.replace(/^["']|["']$/g, "")
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
frontmatter[key] = value
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return { frontmatter, body }
|
|
47
|
-
}
|
package/src/skills/loader.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import * as path from "path"
|
|
2
|
-
import * as os from "os"
|
|
3
|
-
import { SkillRegistry } from "./registry"
|
|
4
|
-
|
|
5
|
-
export function loadAllSkills(projectRoot?: string): SkillRegistry {
|
|
6
|
-
const registry = new SkillRegistry()
|
|
7
|
-
registry.loadAll(projectRoot)
|
|
8
|
-
return registry
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function getDefaultSkillsDirs(): string[] {
|
|
12
|
-
const dirs: string[] = []
|
|
13
|
-
const home = os.homedir()
|
|
14
|
-
|
|
15
|
-
const envDir = process.env.LLMTUNE_SKILLS_DIR
|
|
16
|
-
if (envDir) dirs.push(path.resolve(envDir))
|
|
17
|
-
|
|
18
|
-
dirs.push(path.join(home, ".llmtune", "skills"))
|
|
19
|
-
|
|
20
|
-
return dirs
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function getProjectSkillsDir(projectRoot: string): string {
|
|
24
|
-
return path.join(projectRoot, ".llmtune", "skills")
|
|
25
|
-
}
|
package/src/skills/registry.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs"
|
|
2
|
-
import * as path from "path"
|
|
3
|
-
import * as os from "os"
|
|
4
|
-
import { parseFrontmatter } from "./frontmatter"
|
|
5
|
-
import { substituteArguments } from "./args"
|
|
6
|
-
|
|
7
|
-
export interface Skill {
|
|
8
|
-
name: string
|
|
9
|
-
description: string
|
|
10
|
-
content: string
|
|
11
|
-
loadedFrom: "user" | "project" | "managed"
|
|
12
|
-
userInvocable: boolean
|
|
13
|
-
allowedTools: string[]
|
|
14
|
-
argNames: string[]
|
|
15
|
-
trustLevel: TrustLevel
|
|
16
|
-
skillRoot: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type TrustLevel = "local" | "community" | "verified" | "signed"
|
|
20
|
-
|
|
21
|
-
export interface SkillExecution {
|
|
22
|
-
systemPrompt: string
|
|
23
|
-
userMessage: string
|
|
24
|
-
allowedTools?: string[]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const SKILL_DIRS = () => {
|
|
28
|
-
const dirs: string[] = []
|
|
29
|
-
const primary = path.join(os.homedir(), ".llmtune", "skills")
|
|
30
|
-
dirs.push(primary)
|
|
31
|
-
return dirs
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export class SkillRegistry {
|
|
35
|
-
private skills: Map<string, Skill> = new Map()
|
|
36
|
-
|
|
37
|
-
loadAll(projectRoot?: string): void {
|
|
38
|
-
this.skills.clear()
|
|
39
|
-
|
|
40
|
-
for (const dir of SKILL_DIRS()) {
|
|
41
|
-
this.loadFromDir(dir, "user")
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const managedEnv = process.env.LLMTUNE_MANAGED_SKILLS_DIR
|
|
45
|
-
if (managedEnv) {
|
|
46
|
-
this.loadFromDir(managedEnv, "managed")
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (projectRoot) {
|
|
50
|
-
const projectDir = path.join(projectRoot, ".llmtune", "skills")
|
|
51
|
-
this.loadFromDir(projectDir, "project")
|
|
52
|
-
const compatDir = path.join(projectRoot, ".claude", "skills")
|
|
53
|
-
this.loadFromDir(compatDir, "project")
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private loadFromDir(baseDir: string, loadedFrom: Skill["loadedFrom"]): void {
|
|
58
|
-
const resolved = path.resolve(baseDir)
|
|
59
|
-
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) return
|
|
60
|
-
|
|
61
|
-
for (const entry of fs.readdirSync(resolved).sort()) {
|
|
62
|
-
const skillPath = path.join(resolved, entry)
|
|
63
|
-
if (!fs.statSync(skillPath).isDirectory()) continue
|
|
64
|
-
|
|
65
|
-
const mdPath = path.join(skillPath, "SKILL.md")
|
|
66
|
-
if (!fs.existsSync(mdPath)) continue
|
|
67
|
-
|
|
68
|
-
const raw = fs.readFileSync(mdPath, "utf-8")
|
|
69
|
-
const { frontmatter, body } = parseFrontmatter(raw)
|
|
70
|
-
|
|
71
|
-
const description = String(frontmatter.description ?? extractFirstLine(body) ?? `Skill: ${entry}`)
|
|
72
|
-
const userInvocable = frontmatter["user-invocable"] !== false
|
|
73
|
-
const allowedTools = asStrList(frontmatter["allowed-tools"])
|
|
74
|
-
const argNames = parseArgNames(frontmatter.arguments)
|
|
75
|
-
const trustLevel = resolveTrustLevel(frontmatter.trust, loadedFrom)
|
|
76
|
-
|
|
77
|
-
const skill: Skill = {
|
|
78
|
-
name: entry,
|
|
79
|
-
description,
|
|
80
|
-
content: body,
|
|
81
|
-
loadedFrom,
|
|
82
|
-
userInvocable,
|
|
83
|
-
allowedTools,
|
|
84
|
-
argNames,
|
|
85
|
-
trustLevel,
|
|
86
|
-
skillRoot: skillPath,
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (!this.skills.has(entry.toLowerCase())) {
|
|
90
|
-
this.skills.set(entry.toLowerCase(), skill)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
get(name: string): Skill | undefined {
|
|
96
|
-
return this.skills.get(name.toLowerCase())
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
list(): Skill[] {
|
|
100
|
-
return Array.from(this.skills.values())
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
listUserInvocable(): Skill[] {
|
|
104
|
-
return this.list().filter((s) => s.userInvocable)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
prepareExecution(name: string, args: string[]): SkillExecution | null {
|
|
108
|
-
const skill = this.get(name)
|
|
109
|
-
if (!skill) return null
|
|
110
|
-
|
|
111
|
-
const argMap: Record<string, string> = {}
|
|
112
|
-
for (let i = 0; i < Math.min(args.length, skill.argNames.length); i++) {
|
|
113
|
-
argMap[skill.argNames[i]] = args[i]
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const userMessage = substituteArguments(skill.content, argMap)
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
systemPrompt: `You are executing the "${skill.name}" skill. ${skill.description}`,
|
|
120
|
-
userMessage,
|
|
121
|
-
allowedTools: skill.allowedTools.length > 0 ? skill.allowedTools : undefined,
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function resolveTrustLevel(raw: unknown, loadedFrom: Skill["loadedFrom"]): TrustLevel {
|
|
127
|
-
const str = String(raw ?? "").toLowerCase().trim()
|
|
128
|
-
if (str === "signed") return "signed"
|
|
129
|
-
if (str === "verified") return "verified"
|
|
130
|
-
if (str === "community") return "community"
|
|
131
|
-
if (loadedFrom === "user" || loadedFrom === "project") return "local"
|
|
132
|
-
return "community"
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function extractFirstLine(body: string): string | null {
|
|
136
|
-
for (const line of body.split("\n")) {
|
|
137
|
-
const stripped = line.trim()
|
|
138
|
-
if (!stripped || stripped.startsWith("#")) continue
|
|
139
|
-
return stripped.slice(0, 200)
|
|
140
|
-
}
|
|
141
|
-
return null
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function parseArgNames(raw: unknown): string[] {
|
|
145
|
-
return asStrList(raw)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function asStrList(val: unknown): string[] {
|
|
149
|
-
if (val == null) return []
|
|
150
|
-
if (Array.isArray(val)) return val.map(String).filter(Boolean)
|
|
151
|
-
const s = String(val).trim()
|
|
152
|
-
if (!s) return []
|
|
153
|
-
if (s.includes(",")) return s.split(",").map((x) => x.trim()).filter(Boolean)
|
|
154
|
-
return [s]
|
|
155
|
-
}
|