@llmtune/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +181 -0
  2. package/dist/agent/conversation.d.ts.map +1 -0
  3. package/dist/agent/loop.d.ts.map +1 -0
  4. package/dist/agent/planner.d.ts.map +1 -0
  5. package/dist/auth/client.d.ts.map +1 -0
  6. package/dist/auth/config.d.ts.map +1 -0
  7. package/dist/commands/chat.d.ts.map +1 -0
  8. package/dist/commands/config.d.ts.map +1 -0
  9. package/dist/commands/login.d.ts.map +1 -0
  10. package/dist/commands/marketplace.d.ts.map +1 -0
  11. package/dist/commands/models.d.ts.map +1 -0
  12. package/dist/compact/history-store.d.ts.map +1 -0
  13. package/dist/compact/microcompact.d.ts.map +1 -0
  14. package/dist/compact/service.d.ts.map +1 -0
  15. package/dist/context/analyzer.d.ts.map +1 -0
  16. package/dist/context/builder.d.ts.map +1 -0
  17. package/dist/context/cache.d.ts.map +1 -0
  18. package/dist/context/git-context.d.ts.map +1 -0
  19. package/dist/context/llmtune-md.d.ts.map +1 -0
  20. package/dist/context/workspace.d.ts.map +1 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +125 -0
  23. package/dist/marketplace/client.d.ts.map +1 -0
  24. package/dist/memory/files.d.ts.map +1 -0
  25. package/dist/memory/service.d.ts.map +1 -0
  26. package/dist/repl/repl.d.ts.map +1 -0
  27. package/dist/skills/args.d.ts.map +1 -0
  28. package/dist/skills/frontmatter.d.ts.map +1 -0
  29. package/dist/skills/loader.d.ts.map +1 -0
  30. package/dist/skills/registry.d.ts.map +1 -0
  31. package/dist/skills/signing/signer.d.ts.map +1 -0
  32. package/dist/skills/trust.d.ts.map +1 -0
  33. package/dist/telemetry/logger.d.ts.map +1 -0
  34. package/dist/tools/permissions.d.ts.map +1 -0
  35. package/dist/tools/protocol.d.ts.map +1 -0
  36. package/dist/tools/registry.d.ts.map +1 -0
  37. package/dist/tools/sandbox/docker.d.ts.map +1 -0
  38. package/dist/tools/sandbox/index.d.ts.map +1 -0
  39. package/dist/tools/tools/ask-user.d.ts.map +1 -0
  40. package/dist/tools/tools/bash.d.ts.map +1 -0
  41. package/dist/tools/tools/edit.d.ts.map +1 -0
  42. package/dist/tools/tools/glob.d.ts.map +1 -0
  43. package/dist/tools/tools/grep.d.ts.map +1 -0
  44. package/dist/tools/tools/read.d.ts.map +1 -0
  45. package/dist/tools/tools/web-fetch.d.ts.map +1 -0
  46. package/dist/tools/tools/write.d.ts.map +1 -0
  47. package/dist/tools/validation.d.ts.map +1 -0
  48. package/dist/utils/markdown.d.ts.map +1 -0
  49. package/dist/utils/streaming.d.ts.map +1 -0
  50. package/dist/utils/tokens.d.ts.map +1 -0
  51. package/docs/SKILL_AUTHORING.md +175 -0
  52. package/package.json +38 -0
  53. package/src/agent/conversation.ts +140 -0
  54. package/src/agent/loop.ts +215 -0
  55. package/src/agent/planner.ts +55 -0
  56. package/src/auth/client.ts +19 -0
  57. package/src/auth/config.ts +89 -0
  58. package/src/commands/chat.ts +28 -0
  59. package/src/commands/config.ts +36 -0
  60. package/src/commands/login.ts +63 -0
  61. package/src/commands/marketplace.ts +190 -0
  62. package/src/commands/models.ts +74 -0
  63. package/src/compact/history-store.ts +101 -0
  64. package/src/compact/microcompact.ts +49 -0
  65. package/src/compact/service.ts +154 -0
  66. package/src/context/analyzer.ts +127 -0
  67. package/src/context/builder.ts +123 -0
  68. package/src/context/cache.ts +11 -0
  69. package/src/context/git-context.ts +58 -0
  70. package/src/context/llmtune-md.ts +48 -0
  71. package/src/context/workspace.ts +139 -0
  72. package/src/index.ts +100 -0
  73. package/src/marketplace/client.ts +118 -0
  74. package/src/memory/files.ts +81 -0
  75. package/src/memory/service.ts +124 -0
  76. package/src/repl/repl.ts +400 -0
  77. package/src/skills/args.ts +35 -0
  78. package/src/skills/builtin/explain-code/SKILL.md +30 -0
  79. package/src/skills/frontmatter.ts +47 -0
  80. package/src/skills/loader.ts +25 -0
  81. package/src/skills/registry.ts +155 -0
  82. package/src/skills/signing/signer.ts +101 -0
  83. package/src/skills/trust.ts +50 -0
  84. package/src/telemetry/logger.ts +108 -0
  85. package/src/tools/permissions.ts +83 -0
  86. package/src/tools/protocol.ts +24 -0
  87. package/src/tools/registry.ts +93 -0
  88. package/src/tools/sandbox/docker.ts +225 -0
  89. package/src/tools/sandbox/index.ts +91 -0
  90. package/src/tools/tools/ask-user.ts +60 -0
  91. package/src/tools/tools/bash.ts +97 -0
  92. package/src/tools/tools/edit.ts +111 -0
  93. package/src/tools/tools/glob.ts +68 -0
  94. package/src/tools/tools/grep.ts +121 -0
  95. package/src/tools/tools/read.ts +57 -0
  96. package/src/tools/tools/web-fetch.ts +158 -0
  97. package/src/tools/tools/write.ts +52 -0
  98. package/src/tools/validation.ts +164 -0
  99. package/src/utils/markdown.ts +96 -0
  100. package/src/utils/streaming.ts +63 -0
  101. package/src/utils/tokens.ts +41 -0
  102. package/tsconfig.json +20 -0
package/src/index.ts ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander"
3
+ import chalk from "chalk"
4
+ import { loadConfig, isLoggedIn } from "./auth/config"
5
+ import { createClient } from "./auth/client"
6
+ import { startRepl } from "./repl/repl"
7
+
8
+ const program = new Command()
9
+
10
+ program
11
+ .name("llmtune")
12
+ .description("LLMTune CLI - AI coding agent powered by api.llmtune.io")
13
+ .version("0.1.0")
14
+
15
+ program
16
+ .command("login")
17
+ .description("Configure API key and settings")
18
+ .action(async () => {
19
+ const { loginCommand } = await import("./commands/login")
20
+ await loginCommand()
21
+ })
22
+
23
+ program
24
+ .command("chat")
25
+ .description("Start interactive coding session")
26
+ .option("-m, --model <model>", "Model to use")
27
+ .option("--no-stream", "Disable streaming")
28
+ .action(async (options: { model?: string; stream?: boolean }) => {
29
+ if (!isLoggedIn()) {
30
+ console.log(chalk.red('Not logged in. Run "llmtune login" first.'))
31
+ process.exit(1)
32
+ }
33
+ const config = loadConfig()
34
+ const client = createClient()
35
+ const model = options.model ?? (config.defaultModel as string) ?? "z-ai/GLM-5.1"
36
+ const apiBase = (config.apiBase as string) ?? "https://api.llmtune.io/api/agent/v1"
37
+
38
+ console.log(chalk.cyan("\nLLMTune CLI"))
39
+ console.log(chalk.dim(`Connected to ${apiBase}`))
40
+ console.log(chalk.dim(`Model: ${model}`))
41
+ console.log(chalk.dim("Type /help for commands, /exit to quit\n"))
42
+
43
+ await startRepl({ client, model, stream: options.stream !== false })
44
+ })
45
+
46
+ program
47
+ .command("models")
48
+ .description("List available models")
49
+ .action(async () => {
50
+ const { modelsCommand } = await import("./commands/models")
51
+ await modelsCommand()
52
+ })
53
+
54
+ program
55
+ .command("config")
56
+ .description("Show current configuration")
57
+ .action(async () => {
58
+ const { showConfig } = await import("./commands/config")
59
+ showConfig()
60
+ })
61
+
62
+ // Skills marketplace commands
63
+ const skillsCmd = program.command("skills").description("Manage skills (list, install, publish, sign)")
64
+
65
+ skillsCmd
66
+ .command("list")
67
+ .description("List available skills from marketplace")
68
+ .action(async () => {
69
+ const { listSkillsCommand } = await import("./commands/marketplace")
70
+ await listSkillsCommand()
71
+ })
72
+
73
+ skillsCmd
74
+ .command("install")
75
+ .description("Install a skill from the marketplace")
76
+ .argument("<name>", "Skill name to install")
77
+ .action(async (name: string) => {
78
+ const { installSkillCommand } = await import("./commands/marketplace")
79
+ await installSkillCommand(name)
80
+ })
81
+
82
+ skillsCmd
83
+ .command("publish")
84
+ .description("Publish a local skill to the marketplace")
85
+ .argument("<path>", "Path to skill directory")
86
+ .action(async (skillPath: string) => {
87
+ const { publishSkillCommand } = await import("./commands/marketplace")
88
+ await publishSkillCommand(skillPath)
89
+ })
90
+
91
+ skillsCmd
92
+ .command("sign")
93
+ .description("Cryptographically sign a skill")
94
+ .argument("<path>", "Path to skill directory")
95
+ .action(async (skillPath: string) => {
96
+ const { signSkillCommand } = await import("./commands/marketplace")
97
+ await signSkillCommand(skillPath)
98
+ })
99
+
100
+ program.parse()
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Skill marketplace client.
3
+ * Interacts with the infra's skill registry API to list, search, install, and publish skills.
4
+ */
5
+ import type { AppConfig } from "../auth/config"
6
+
7
+ export interface MarketplaceSkill {
8
+ name: string
9
+ description: string
10
+ trustLevel: "local" | "community" | "verified" | "signed"
11
+ allowedTools: string[]
12
+ author?: string
13
+ version?: string
14
+ installs?: number
15
+ rating?: number
16
+ tags?: string[]
17
+ }
18
+
19
+ export interface SearchResult {
20
+ skills: MarketplaceSkill[]
21
+ total: number
22
+ page: number
23
+ }
24
+
25
+ function getApiBase(config: AppConfig): string {
26
+ return String(config.apiBase ?? "https://api.llmtune.io/api/agent/v1").replace(/\/$/, "")
27
+ }
28
+
29
+ function getHeaders(config: AppConfig): Record<string, string> {
30
+ return {
31
+ "Content-Type": "application/json",
32
+ Authorization: `Bearer ${config.apiKey}`,
33
+ }
34
+ }
35
+
36
+ /**
37
+ * List all skills in the marketplace.
38
+ */
39
+ export async function listSkills(
40
+ config: AppConfig,
41
+ options?: { search?: string; tag?: string; page?: number; limit?: number },
42
+ ): Promise<SearchResult> {
43
+ const base = getApiBase(config)
44
+ const params = new URLSearchParams()
45
+ if (options?.search) params.set("search", options.search)
46
+ if (options?.tag) params.set("tag", options.tag)
47
+ if (options?.page) params.set("page", String(options.page))
48
+ if (options?.limit) params.set("limit", String(options.limit))
49
+
50
+ const url = `${base}/skills?${params.toString()}`
51
+ const response = await fetch(url, { headers: getHeaders(config) })
52
+
53
+ if (!response.ok) {
54
+ throw new Error(`Failed to list skills: HTTP ${response.status}`)
55
+ }
56
+
57
+ return (await response.json()) as SearchResult
58
+ }
59
+
60
+ /**
61
+ * Get details for a specific skill.
62
+ */
63
+ export async function getSkillDetails(config: AppConfig, name: string): Promise<MarketplaceSkill> {
64
+ const base = getApiBase(config)
65
+ const url = `${base}/skills/${encodeURIComponent(name)}`
66
+ const response = await fetch(url, { headers: getHeaders(config) })
67
+
68
+ if (!response.ok) {
69
+ if (response.status === 404) throw new Error(`Skill not found: ${name}`)
70
+ throw new Error(`Failed to get skill: HTTP ${response.status}`)
71
+ }
72
+
73
+ return (await response.json()) as MarketplaceSkill
74
+ }
75
+
76
+ /**
77
+ * Install a skill from the marketplace to the local skills directory.
78
+ */
79
+ export async function installSkill(config: AppConfig, name: string): Promise<string> {
80
+ const base = getApiBase(config)
81
+ const url = `${base}/skills/install`
82
+ const response = await fetch(url, {
83
+ method: "POST",
84
+ headers: getHeaders(config),
85
+ body: JSON.stringify({ skillName: name }),
86
+ })
87
+
88
+ if (!response.ok) {
89
+ const body = await response.text()
90
+ throw new Error(`Failed to install skill: ${body}`)
91
+ }
92
+
93
+ const data = (await response.json()) as { installPath: string; skillName: string }
94
+ return data.installPath
95
+ }
96
+
97
+ /**
98
+ * Publish a local skill to the marketplace.
99
+ */
100
+ export async function publishSkill(
101
+ config: AppConfig,
102
+ skill: { name: string; description: string; content: string; trustLevel: string; allowedTools: string[] },
103
+ ): Promise<{ published: boolean; skillName: string }> {
104
+ const base = getApiBase(config)
105
+ const url = `${base}/skills`
106
+ const response = await fetch(url, {
107
+ method: "POST",
108
+ headers: getHeaders(config),
109
+ body: JSON.stringify(skill),
110
+ })
111
+
112
+ if (!response.ok) {
113
+ const body = await response.text()
114
+ throw new Error(`Failed to publish skill: ${body}`)
115
+ }
116
+
117
+ return (await response.json()) as { published: boolean; skillName: string }
118
+ }
@@ -0,0 +1,81 @@
1
+ import * as fs from "fs"
2
+ import * as path from "path"
3
+ import { getConfigDir } from "../auth/config"
4
+
5
+ export interface MemoryCategory {
6
+ key: string
7
+ filename: string
8
+ description: string
9
+ }
10
+
11
+ export const MEMORY_CATEGORIES: MemoryCategory[] = [
12
+ { key: "preferences", filename: "preferences.md", description: "User preferences (language, style, conventions)" },
13
+ { key: "project", filename: "project-notes.md", description: "Project knowledge (architecture, patterns, key decisions)" },
14
+ { key: "decisions", filename: "decisions.md", description: "Past decisions and rationale" },
15
+ { key: "architecture", filename: "architecture.md", description: "System architecture notes" },
16
+ ]
17
+
18
+ export function getMemoryDir(): string {
19
+ return path.join(getConfigDir(), "memory")
20
+ }
21
+
22
+ export function ensureMemoryDir(): void {
23
+ const dir = getMemoryDir()
24
+ if (!fs.existsSync(dir)) {
25
+ fs.mkdirSync(dir, { recursive: true })
26
+ }
27
+ for (const cat of MEMORY_CATEGORIES) {
28
+ const filePath = path.join(dir, cat.filename)
29
+ if (!fs.existsSync(filePath)) {
30
+ fs.writeFileSync(filePath, `# ${cat.description}\n\n`, "utf-8")
31
+ }
32
+ }
33
+ }
34
+
35
+ export function readMemory(key: string): string {
36
+ const cat = MEMORY_CATEGORIES.find((c) => c.key === key)
37
+ if (!cat) return ""
38
+ const filePath = path.join(getMemoryDir(), cat.filename)
39
+ try {
40
+ return fs.readFileSync(filePath, "utf-8").trim()
41
+ } catch {
42
+ return ""
43
+ }
44
+ }
45
+
46
+ export function writeMemory(key: string, content: string): void {
47
+ const cat = MEMORY_CATEGORIES.find((c) => c.key === key)
48
+ if (!cat) return
49
+ ensureMemoryDir()
50
+ const filePath = path.join(getMemoryDir(), cat.filename)
51
+ fs.writeFileSync(filePath, content, "utf-8")
52
+ }
53
+
54
+ export function appendMemory(key: string, line: string): void {
55
+ const current = readMemory(key)
56
+ writeMemory(key, current + "\n" + line)
57
+ }
58
+
59
+ export function readAllMemory(): Record<string, string> {
60
+ const result: Record<string, string> = {}
61
+ for (const cat of MEMORY_CATEGORIES) {
62
+ const content = readMemory(cat.key)
63
+ if (content) result[cat.key] = content
64
+ }
65
+ return result
66
+ }
67
+
68
+ export function buildMemoryPrompt(): string {
69
+ const memories = readAllMemory()
70
+ const entries = Object.entries(memories)
71
+ if (entries.length === 0) return ""
72
+ const lines = ["## User Memory"]
73
+ for (const [key, content] of entries) {
74
+ const cat = MEMORY_CATEGORIES.find((c) => c.key === key)
75
+ if (cat && content.trim()) {
76
+ lines.push(`### ${cat.description}`)
77
+ lines.push(content)
78
+ }
79
+ }
80
+ return lines.join("\n")
81
+ }
@@ -0,0 +1,124 @@
1
+ import * as fs from "fs"
2
+ import * as path from "path"
3
+ import * as os from "os"
4
+
5
+ const MEMORY_DIR = path.join(os.homedir(), ".llmtune", "memory")
6
+
7
+ const MEMORY_FILES = {
8
+ preferences: "preferences.md",
9
+ "project-notes": "project-notes.md",
10
+ decisions: "decisions.md",
11
+ architecture: "architecture.md",
12
+ } as const
13
+
14
+ type MemoryCategory = keyof typeof MEMORY_FILES
15
+
16
+ export interface MemoryEntry {
17
+ category: MemoryCategory
18
+ content: string
19
+ path: string
20
+ }
21
+
22
+ function ensureMemoryDir(): void {
23
+ if (!fs.existsSync(MEMORY_DIR)) {
24
+ fs.mkdirSync(MEMORY_DIR, { recursive: true })
25
+ }
26
+ }
27
+
28
+ function getMemoryPath(category: MemoryCategory): string {
29
+ return path.join(MEMORY_DIR, MEMORY_FILES[category])
30
+ }
31
+
32
+ export function readMemory(category: MemoryCategory): string {
33
+ const filePath = getMemoryPath(category)
34
+ try {
35
+ return fs.readFileSync(filePath, "utf-8").trim()
36
+ } catch {
37
+ return ""
38
+ }
39
+ }
40
+
41
+ export function writeMemory(category: MemoryCategory, content: string): void {
42
+ ensureMemoryDir()
43
+ const filePath = getMemoryPath(category)
44
+ fs.writeFileSync(filePath, content.trim() + "\n", "utf-8")
45
+ }
46
+
47
+ export function appendMemory(category: MemoryCategory, line: string): void {
48
+ ensureMemoryDir()
49
+ const existing = readMemory(category)
50
+ const updated = existing ? `${existing}\n${line}` : line
51
+ writeMemory(category, updated)
52
+ }
53
+
54
+ export function readAllMemory(): MemoryEntry[] {
55
+ ensureMemoryDir()
56
+ const entries: MemoryEntry[] = []
57
+ for (const [category, filename] of Object.entries(MEMORY_FILES)) {
58
+ const content = readMemory(category as MemoryCategory)
59
+ if (content) {
60
+ entries.push({
61
+ category: category as MemoryCategory,
62
+ content,
63
+ path: path.join(MEMORY_DIR, filename),
64
+ })
65
+ }
66
+ }
67
+ return entries
68
+ }
69
+
70
+ export function buildMemoryPrompt(): string {
71
+ const entries = readAllMemory()
72
+ if (entries.length === 0) return ""
73
+
74
+ const sections = entries.map((entry) => {
75
+ const label = entry.category.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
76
+ return `### ${label}\n${entry.content}`
77
+ })
78
+
79
+ return "## User Memory\n\n" + sections.join("\n\n")
80
+ }
81
+
82
+ export function clearMemory(category?: MemoryCategory): void {
83
+ if (category) {
84
+ const filePath = getMemoryPath(category)
85
+ try {
86
+ fs.unlinkSync(filePath)
87
+ } catch {
88
+ // file doesn't exist
89
+ }
90
+ } else {
91
+ for (const filename of Object.values(MEMORY_FILES)) {
92
+ try {
93
+ fs.unlinkSync(path.join(MEMORY_DIR, filename))
94
+ } catch {
95
+ // skip
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ export function getMemoryDir(): string {
102
+ return MEMORY_DIR
103
+ }
104
+
105
+ export function initMemoryFiles(): void {
106
+ ensureMemoryDir()
107
+ const defaults: Record<MemoryCategory, string> = {
108
+ preferences:
109
+ "# User Preferences\n# Add your coding preferences here (one per line)\n# Example: I prefer TypeScript over JavaScript\n# Example: I use 2-space indentation\n",
110
+ "project-notes":
111
+ "# Project Notes\n# Key facts about the current project\n# Example: Auth uses JWT + bcrypt\n# Example: Database is Neon PostgreSQL via Prisma\n",
112
+ decisions:
113
+ "# Architecture Decisions\n# Record important technical decisions\n# Example: Decided to use Prisma instead of Drizzle for ORM\n",
114
+ architecture:
115
+ "# Architecture Overview\n# Describe the project structure\n# Example: Frontend: Next.js 16, Backend: Express 5, DB: Neon\n",
116
+ }
117
+
118
+ for (const [category, defaultContent] of Object.entries(defaults)) {
119
+ const filePath = getMemoryPath(category as MemoryCategory)
120
+ if (!fs.existsSync(filePath)) {
121
+ fs.writeFileSync(filePath, defaultContent, "utf-8")
122
+ }
123
+ }
124
+ }