@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.
Files changed (198) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/conversation.d.ts +42 -0
  3. package/dist/agent/conversation.js +105 -0
  4. package/dist/agent/loop.d.ts +19 -0
  5. package/dist/agent/loop.js +185 -0
  6. package/dist/agent/planner.d.ts +8 -0
  7. package/dist/agent/planner.js +43 -0
  8. package/dist/auth/client.d.ts +4 -0
  9. package/dist/auth/client.js +24 -0
  10. package/dist/auth/config.d.ts +21 -0
  11. package/dist/auth/config.js +83 -0
  12. package/dist/commands/chat.d.ts +5 -0
  13. package/dist/commands/chat.js +27 -0
  14. package/dist/commands/config.d.ts +2 -0
  15. package/dist/commands/config.js +37 -0
  16. package/dist/commands/login.d.ts +2 -0
  17. package/dist/commands/login.js +93 -0
  18. package/dist/commands/marketplace.d.ts +6 -0
  19. package/dist/commands/marketplace.js +213 -0
  20. package/dist/commands/models.d.ts +2 -0
  21. package/dist/commands/models.js +53 -0
  22. package/dist/compact/history-store.d.ts +29 -0
  23. package/dist/compact/history-store.js +110 -0
  24. package/dist/compact/microcompact.d.ts +10 -0
  25. package/dist/compact/microcompact.js +43 -0
  26. package/dist/compact/service.d.ts +13 -0
  27. package/dist/compact/service.js +156 -0
  28. package/dist/context/analyzer.d.ts +26 -0
  29. package/dist/context/analyzer.js +99 -0
  30. package/dist/context/builder.d.ts +13 -0
  31. package/dist/context/builder.js +144 -0
  32. package/dist/context/cache.d.ts +6 -0
  33. package/dist/context/cache.js +8 -0
  34. package/dist/context/git-context.d.ts +9 -0
  35. package/dist/context/git-context.js +39 -0
  36. package/dist/context/llmtune-md.d.ts +6 -0
  37. package/dist/context/llmtune-md.js +73 -0
  38. package/dist/context/workspace.d.ts +11 -0
  39. package/dist/context/workspace.js +115 -0
  40. package/dist/index.d.ts +3 -0
  41. package/dist/index.js +3 -2
  42. package/dist/marketplace/client.d.ts +52 -0
  43. package/dist/marketplace/client.js +86 -0
  44. package/dist/memory/files.d.ts +14 -0
  45. package/dist/memory/files.js +116 -0
  46. package/dist/memory/service.d.ts +22 -0
  47. package/dist/memory/service.js +146 -0
  48. package/dist/repl/repl.d.ts +8 -0
  49. package/dist/repl/repl.js +375 -0
  50. package/dist/skills/args.d.ts +10 -0
  51. package/dist/skills/args.js +37 -0
  52. package/dist/skills/frontmatter.d.ts +6 -0
  53. package/dist/skills/frontmatter.js +44 -0
  54. package/dist/skills/loader.d.ts +5 -0
  55. package/dist/skills/loader.js +59 -0
  56. package/dist/skills/registry.d.ts +27 -0
  57. package/dist/skills/registry.js +162 -0
  58. package/dist/skills/signing/signer.d.ts +19 -0
  59. package/dist/skills/signing/signer.js +110 -0
  60. package/dist/skills/trust.d.ts +11 -0
  61. package/dist/skills/trust.js +42 -0
  62. package/dist/telemetry/logger.d.ts +51 -0
  63. package/dist/telemetry/logger.js +135 -0
  64. package/dist/tools/permissions.d.ts +20 -0
  65. package/dist/tools/permissions.js +58 -0
  66. package/dist/tools/protocol.d.ts +22 -0
  67. package/dist/tools/protocol.js +3 -0
  68. package/dist/tools/registry.d.ts +20 -0
  69. package/dist/tools/registry.js +77 -0
  70. package/dist/tools/sandbox/docker.d.ts +16 -0
  71. package/dist/tools/sandbox/docker.js +240 -0
  72. package/dist/tools/sandbox/index.d.ts +18 -0
  73. package/dist/tools/sandbox/index.js +80 -0
  74. package/dist/tools/tools/ask-user.d.ts +3 -0
  75. package/dist/tools/tools/ask-user.js +56 -0
  76. package/dist/tools/tools/bash.d.ts +3 -0
  77. package/dist/tools/tools/bash.js +85 -0
  78. package/dist/tools/tools/edit.d.ts +3 -0
  79. package/dist/tools/tools/edit.js +138 -0
  80. package/dist/tools/tools/glob.d.ts +3 -0
  81. package/dist/tools/tools/glob.js +63 -0
  82. package/dist/tools/tools/grep.d.ts +3 -0
  83. package/dist/tools/tools/grep.js +148 -0
  84. package/dist/tools/tools/read.d.ts +3 -0
  85. package/dist/tools/tools/read.js +85 -0
  86. package/dist/tools/tools/web-fetch.d.ts +3 -0
  87. package/dist/tools/tools/web-fetch.js +143 -0
  88. package/dist/tools/tools/write.d.ts +3 -0
  89. package/dist/tools/tools/write.js +84 -0
  90. package/dist/tools/validation.d.ts +13 -0
  91. package/dist/tools/validation.js +142 -0
  92. package/dist/utils/markdown.d.ts +9 -0
  93. package/dist/utils/markdown.js +89 -0
  94. package/dist/utils/streaming.d.ts +10 -0
  95. package/dist/utils/streaming.js +63 -0
  96. package/dist/utils/tokens.d.ts +12 -0
  97. package/dist/utils/tokens.js +44 -0
  98. package/dist/version.d.ts +2 -0
  99. package/dist/version.js +9 -0
  100. package/package.json +2 -2
  101. package/dist/agent/conversation.d.ts.map +0 -1
  102. package/dist/agent/loop.d.ts.map +0 -1
  103. package/dist/agent/planner.d.ts.map +0 -1
  104. package/dist/auth/client.d.ts.map +0 -1
  105. package/dist/auth/config.d.ts.map +0 -1
  106. package/dist/commands/chat.d.ts.map +0 -1
  107. package/dist/commands/config.d.ts.map +0 -1
  108. package/dist/commands/login.d.ts.map +0 -1
  109. package/dist/commands/marketplace.d.ts.map +0 -1
  110. package/dist/commands/models.d.ts.map +0 -1
  111. package/dist/compact/history-store.d.ts.map +0 -1
  112. package/dist/compact/microcompact.d.ts.map +0 -1
  113. package/dist/compact/service.d.ts.map +0 -1
  114. package/dist/context/analyzer.d.ts.map +0 -1
  115. package/dist/context/builder.d.ts.map +0 -1
  116. package/dist/context/cache.d.ts.map +0 -1
  117. package/dist/context/git-context.d.ts.map +0 -1
  118. package/dist/context/llmtune-md.d.ts.map +0 -1
  119. package/dist/context/workspace.d.ts.map +0 -1
  120. package/dist/index.d.ts.map +0 -1
  121. package/dist/marketplace/client.d.ts.map +0 -1
  122. package/dist/memory/files.d.ts.map +0 -1
  123. package/dist/memory/service.d.ts.map +0 -1
  124. package/dist/repl/repl.d.ts.map +0 -1
  125. package/dist/skills/args.d.ts.map +0 -1
  126. package/dist/skills/frontmatter.d.ts.map +0 -1
  127. package/dist/skills/loader.d.ts.map +0 -1
  128. package/dist/skills/registry.d.ts.map +0 -1
  129. package/dist/skills/signing/signer.d.ts.map +0 -1
  130. package/dist/skills/trust.d.ts.map +0 -1
  131. package/dist/telemetry/logger.d.ts.map +0 -1
  132. package/dist/tools/permissions.d.ts.map +0 -1
  133. package/dist/tools/protocol.d.ts.map +0 -1
  134. package/dist/tools/registry.d.ts.map +0 -1
  135. package/dist/tools/sandbox/docker.d.ts.map +0 -1
  136. package/dist/tools/sandbox/index.d.ts.map +0 -1
  137. package/dist/tools/tools/ask-user.d.ts.map +0 -1
  138. package/dist/tools/tools/bash.d.ts.map +0 -1
  139. package/dist/tools/tools/edit.d.ts.map +0 -1
  140. package/dist/tools/tools/glob.d.ts.map +0 -1
  141. package/dist/tools/tools/grep.d.ts.map +0 -1
  142. package/dist/tools/tools/read.d.ts.map +0 -1
  143. package/dist/tools/tools/web-fetch.d.ts.map +0 -1
  144. package/dist/tools/tools/write.d.ts.map +0 -1
  145. package/dist/tools/validation.d.ts.map +0 -1
  146. package/dist/utils/markdown.d.ts.map +0 -1
  147. package/dist/utils/streaming.d.ts.map +0 -1
  148. package/dist/utils/tokens.d.ts.map +0 -1
  149. package/src/agent/conversation.ts +0 -140
  150. package/src/agent/loop.ts +0 -215
  151. package/src/agent/planner.ts +0 -55
  152. package/src/auth/client.ts +0 -19
  153. package/src/auth/config.ts +0 -89
  154. package/src/commands/chat.ts +0 -28
  155. package/src/commands/config.ts +0 -36
  156. package/src/commands/login.ts +0 -63
  157. package/src/commands/marketplace.ts +0 -190
  158. package/src/commands/models.ts +0 -74
  159. package/src/compact/history-store.ts +0 -101
  160. package/src/compact/microcompact.ts +0 -49
  161. package/src/compact/service.ts +0 -154
  162. package/src/context/analyzer.ts +0 -127
  163. package/src/context/builder.ts +0 -123
  164. package/src/context/cache.ts +0 -11
  165. package/src/context/git-context.ts +0 -58
  166. package/src/context/llmtune-md.ts +0 -48
  167. package/src/context/workspace.ts +0 -139
  168. package/src/index.ts +0 -100
  169. package/src/marketplace/client.ts +0 -118
  170. package/src/memory/files.ts +0 -81
  171. package/src/memory/service.ts +0 -124
  172. package/src/repl/repl.ts +0 -400
  173. package/src/skills/args.ts +0 -35
  174. package/src/skills/builtin/explain-code/SKILL.md +0 -30
  175. package/src/skills/frontmatter.ts +0 -47
  176. package/src/skills/loader.ts +0 -25
  177. package/src/skills/registry.ts +0 -155
  178. package/src/skills/signing/signer.ts +0 -101
  179. package/src/skills/trust.ts +0 -50
  180. package/src/telemetry/logger.ts +0 -108
  181. package/src/tools/permissions.ts +0 -83
  182. package/src/tools/protocol.ts +0 -24
  183. package/src/tools/registry.ts +0 -93
  184. package/src/tools/sandbox/docker.ts +0 -225
  185. package/src/tools/sandbox/index.ts +0 -91
  186. package/src/tools/tools/ask-user.ts +0 -60
  187. package/src/tools/tools/bash.ts +0 -97
  188. package/src/tools/tools/edit.ts +0 -111
  189. package/src/tools/tools/glob.ts +0 -68
  190. package/src/tools/tools/grep.ts +0 -121
  191. package/src/tools/tools/read.ts +0 -57
  192. package/src/tools/tools/web-fetch.ts +0 -158
  193. package/src/tools/tools/write.ts +0 -52
  194. package/src/tools/validation.ts +0 -164
  195. package/src/utils/markdown.ts +0 -96
  196. package/src/utils/streaming.ts +0 -63
  197. package/src/utils/tokens.ts +0 -41
  198. 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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }