@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
@@ -0,0 +1,89 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import os from "os"
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), ".llmtune")
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json")
7
+
8
+ export interface AuthConfig {
9
+ apiKey: string
10
+ apiBase: string
11
+ defaultModel?: string
12
+ }
13
+
14
+ export interface AppConfig {
15
+ defaultProvider?: string
16
+ providers?: Record<string, AuthConfig>
17
+ [key: string]: unknown
18
+ }
19
+
20
+ export function ensureConfigDir(): void {
21
+ if (!fs.existsSync(CONFIG_DIR)) {
22
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
23
+ }
24
+ const memDir = path.join(CONFIG_DIR, "memory")
25
+ if (!fs.existsSync(memDir)) {
26
+ fs.mkdirSync(memDir, { recursive: true })
27
+ }
28
+ const sessionsDir = path.join(CONFIG_DIR, "sessions")
29
+ if (!fs.existsSync(sessionsDir)) {
30
+ fs.mkdirSync(sessionsDir, { recursive: true })
31
+ }
32
+ const logsDir = path.join(CONFIG_DIR, "logs")
33
+ if (!fs.existsSync(logsDir)) {
34
+ fs.mkdirSync(logsDir, { recursive: true })
35
+ }
36
+ }
37
+
38
+ export function loadConfig(): AppConfig {
39
+ ensureConfigDir()
40
+ if (!fs.existsSync(CONFIG_FILE)) {
41
+ return {}
42
+ }
43
+ try {
44
+ const raw = fs.readFileSync(CONFIG_FILE, "utf-8")
45
+ return JSON.parse(raw) as AppConfig
46
+ } catch {
47
+ return {}
48
+ }
49
+ }
50
+
51
+ export function saveConfig(config: AppConfig): void {
52
+ ensureConfigDir()
53
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8")
54
+ }
55
+
56
+ export function getApiKey(): string | null {
57
+ const config = loadConfig()
58
+ return (config.apiKey as string) ?? null
59
+ }
60
+
61
+ export function getApiBase(): string {
62
+ const config = loadConfig()
63
+ return (config.apiBase as string) ?? "https://api.llmtune.io/api/agent/v1"
64
+ }
65
+
66
+ export function getDefaultModel(): string {
67
+ const config = loadConfig()
68
+ return (config.defaultModel as string) ?? "z-ai/GLM-5.1"
69
+ }
70
+
71
+ export function isLoggedIn(): boolean {
72
+ return getApiKey() !== null
73
+ }
74
+
75
+ export function logout(): void {
76
+ const config = loadConfig()
77
+ delete config.apiKey
78
+ delete config.apiBase
79
+ delete config.defaultModel
80
+ saveConfig(config)
81
+ }
82
+
83
+ export function getConfigPath(): string {
84
+ return CONFIG_FILE
85
+ }
86
+
87
+ export function getConfigDir(): string {
88
+ return CONFIG_DIR
89
+ }
@@ -0,0 +1,28 @@
1
+ import chalk from "chalk";
2
+ import { loadConfig, getApiBase } from "../auth/config.js";
3
+ import { createClient } from "../auth/client.js";
4
+ import { startRepl } from "../repl/repl.js";
5
+
6
+ export async function chatCommand(options: {
7
+ model?: string;
8
+ stream?: boolean;
9
+ }) {
10
+ const config = loadConfig();
11
+
12
+ if (!config.apiKey) {
13
+ console.log(chalk.red('No API key configured. Run "llmtune login" first.'));
14
+ process.exit(1);
15
+ }
16
+
17
+ const client = createClient();
18
+ const model = options.model || (config.defaultModel as string) || "z-ai/GLM-5.1";
19
+ const stream = options.stream !== false;
20
+
21
+ console.log(chalk.cyan(`\nLLMTune CLI`));
22
+ console.log(chalk.dim(`Connected to ${getApiBase()}`));
23
+ console.log(chalk.dim(`Model: ${model}`));
24
+ console.log(chalk.dim(`Stream: ${stream}`));
25
+ console.log(chalk.dim(`Type /help for commands, /exit to quit\n`));
26
+
27
+ await startRepl({ client, model, stream });
28
+ }
@@ -0,0 +1,36 @@
1
+ import chalk from "chalk";
2
+ import { loadConfig, getConfigPath } from "../auth/config";
3
+
4
+ export function showConfig(): void {
5
+ const configPath = getConfigPath();
6
+
7
+ try {
8
+ const config = loadConfig();
9
+ console.log(chalk.bold("\nConfiguration\n"));
10
+ console.log(` Config file: ${chalk.cyan(configPath)}`);
11
+ const apiKey = config.apiKey as string | undefined
12
+ const apiBase = config.apiBase as string | undefined
13
+ const defaultModel = config.defaultModel as string | undefined
14
+ console.log(` API base: ${chalk.cyan(apiBase ?? "(not set)")}`);
15
+ console.log(` Default model: ${chalk.cyan(defaultModel || "(not set)")}`);
16
+
17
+ if (apiKey) {
18
+ const masked =
19
+ apiKey.length > 12
20
+ ? `${apiKey.slice(0, 6)}...${apiKey.slice(-4)}`
21
+ : "(set)";
22
+ console.log(` API key: ${chalk.green(masked)}`);
23
+ } else {
24
+ console.log(` API key: ${chalk.red("(not set)")}`);
25
+ console.log(
26
+ chalk.dim(`\n Run ${chalk.bold("llmtune login")} to configure your API key.`)
27
+ );
28
+ }
29
+ console.log();
30
+ } catch {
31
+ console.log(chalk.dim(`\n No config found at ${configPath}`));
32
+ console.log(
33
+ chalk.dim(` Run ${chalk.bold("llmtune login")} to get started.\n`)
34
+ );
35
+ }
36
+ }
@@ -0,0 +1,63 @@
1
+ import * as readline from "readline"
2
+ import chalk from "chalk"
3
+ import { saveConfig, loadConfig, getConfigPath } from "../auth/config"
4
+
5
+ function ask(question: string, defaultValue?: string): Promise<string> {
6
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
7
+ const prompt = defaultValue ? `${question} (${defaultValue})` : question
8
+ return new Promise((resolve) => {
9
+ rl.question(`${prompt}: `, (answer) => {
10
+ rl.close()
11
+ resolve(answer.trim() || defaultValue || "")
12
+ })
13
+ })
14
+ }
15
+
16
+ function askPassword(question: string): Promise<string> {
17
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
18
+ return new Promise((resolve) => {
19
+ rl.question(`${question}: `, (answer) => {
20
+ rl.close()
21
+ resolve(answer.trim())
22
+ })
23
+ })
24
+ }
25
+
26
+ function maskKey(key: string): string {
27
+ if (key.length <= 12) return "sk_***"
28
+ return `${key.slice(0, 5)}...${key.slice(-4)}`
29
+ }
30
+
31
+ export async function loginCommand(): Promise<void> {
32
+ console.log(chalk.blue.bold("\nLLMTune CLI - API Configuration\n"))
33
+
34
+ const configPath = getConfigPath()
35
+ const existing = loadConfig()
36
+
37
+ const existingKey = (existing.apiKey as string) || ""
38
+ const existingUrl = (existing.apiBase as string) || "https://api.llmtune.io/api/agent/v1"
39
+ const existingModel = (existing.defaultModel as string) || ""
40
+
41
+ console.log(chalk.dim(`Config file: ${configPath}\n`))
42
+
43
+ const key = await askPassword("Enter your LLMTune API key (sk_...)")
44
+ if (!key) {
45
+ console.log(chalk.red("\nAPI key is required."))
46
+ process.exit(1)
47
+ }
48
+
49
+ const apiBase = await ask("API base URL", existingUrl)
50
+ const model = await ask("Default model (leave empty for auto)", existingModel || undefined)
51
+
52
+ saveConfig({
53
+ apiKey: key,
54
+ apiBase: apiBase.replace(/\/$/, ""),
55
+ defaultModel: model || undefined,
56
+ })
57
+
58
+ console.log(chalk.green("\nConfiguration saved!"))
59
+ console.log(` API key: ${maskKey(key)}`)
60
+ console.log(` API base: ${apiBase}`)
61
+ console.log(` Model: ${model || "auto"}`)
62
+ console.log(chalk.dim(`\nRun ${chalk.bold("llmtune chat")} to start.\n`))
63
+ }
@@ -0,0 +1,190 @@
1
+ import chalk from "chalk"
2
+ import { createInterface } from "readline"
3
+ import { listSkills, getSkillDetails, installSkill, publishSkill } from "../marketplace/client"
4
+ import { loadConfig } from "../auth/config"
5
+ import type { AppConfig } from "../auth/config"
6
+
7
+ export async function marketplaceCommand(action: string, args: string[]): Promise<void> {
8
+ const config = loadConfig()
9
+ const apiKey = config.apiKey as string | undefined
10
+ if (!apiKey) {
11
+ console.log(chalk.red('Not logged in. Run "llmtune login" first.'))
12
+ process.exit(1)
13
+ }
14
+
15
+ switch (action) {
16
+ case "search":
17
+ case "list":
18
+ await searchSkills(config, args[0] ?? "")
19
+ break
20
+ case "info":
21
+ if (!args[0]) {
22
+ console.log(chalk.yellow("Usage: llmtune skills info <skill-name>"))
23
+ break
24
+ }
25
+ await showSkillInfo(config, args[0])
26
+ break
27
+ case "install":
28
+ if (!args[0]) {
29
+ console.log(chalk.yellow("Usage: llmtune skills install <skill-name>"))
30
+ break
31
+ }
32
+ await doInstallSkill(config, args[0])
33
+ break
34
+ case "publish":
35
+ if (!args[0]) {
36
+ console.log(chalk.yellow("Usage: llmtune skills publish <skill-dir>"))
37
+ break
38
+ }
39
+ await doPublishSkill(config, args[0])
40
+ break
41
+ default:
42
+ console.log(chalk.yellow(`Unknown action: ${action}`))
43
+ console.log(chalk.dim("Available: search, info, install, publish"))
44
+ }
45
+ }
46
+
47
+ async function searchSkills(config: AppConfig, query: string): Promise<void> {
48
+ console.log(chalk.cyan(`\nSearching skills${query ? `: ${query}` : ""}...\n`))
49
+
50
+ const result = await listSkills(config, { search: query || undefined })
51
+ const skills = result.skills
52
+
53
+ if (skills.length === 0) {
54
+ console.log(chalk.dim("No skills found."))
55
+ return
56
+ }
57
+
58
+ for (const skill of skills) {
59
+ const trust = chalk.dim(`[${skill.trustLevel}]`)
60
+ const installs = skill.installs ? chalk.dim(`(${skill.installs} installs)`) : ""
61
+ console.log(` ${chalk.bold(skill.name)} ${trust} ${installs}`)
62
+ console.log(` ${chalk.dim(skill.description)}`)
63
+ if (skill.author) {
64
+ console.log(` ${chalk.dim(`by ${skill.author}`)}`)
65
+ }
66
+ console.log()
67
+ }
68
+
69
+ console.log(
70
+ chalk.dim(
71
+ `${skills.length} of ${result.total} skills shown. Use ${chalk.bold("llmtune skills install <name>")} to install.`,
72
+ ),
73
+ )
74
+ }
75
+
76
+ async function showSkillInfo(config: AppConfig, name: string): Promise<void> {
77
+ try {
78
+ const skill = await getSkillDetails(config, name)
79
+ console.log(chalk.bold(`\n${skill.name}`))
80
+ console.log(chalk.dim(skill.description))
81
+ console.log()
82
+ console.log(` Trust: ${skill.trustLevel}`)
83
+ console.log(` Author: ${skill.author ?? "unknown"}`)
84
+ if (skill.installs !== undefined) console.log(` Installs: ${skill.installs}`)
85
+ if (skill.rating !== undefined) {
86
+ const stars = "\u2605".repeat(Math.round(skill.rating)) + "\u2606".repeat(5 - Math.round(skill.rating))
87
+ console.log(` Rating: ${stars} (${skill.rating.toFixed(1)})`)
88
+ }
89
+ if (skill.allowedTools.length > 0) {
90
+ console.log(` Tools: ${skill.allowedTools.join(", ")}`)
91
+ }
92
+ if (skill.tags && skill.tags.length > 0) {
93
+ console.log(` Tags: ${skill.tags.join(", ")}`)
94
+ }
95
+ console.log()
96
+ } catch (err: unknown) {
97
+ const msg = err instanceof Error ? err.message : String(err)
98
+ console.log(chalk.red(`Error: ${msg}`))
99
+ }
100
+ }
101
+
102
+ async function doInstallSkill(config: AppConfig, name: string): Promise<void> {
103
+ console.log(chalk.cyan(`Installing skill: ${name}...`))
104
+
105
+ try {
106
+ const skillPath = await installSkill(config, name)
107
+ console.log(chalk.green(`\nSkill "${name}" installed to: ${skillPath}`))
108
+ console.log(chalk.dim(`Run /${name} in a chat session to use it.`))
109
+ } catch (err: unknown) {
110
+ const msg = err instanceof Error ? err.message : String(err)
111
+ console.log(chalk.red(`\nFailed to install: ${msg}`))
112
+ }
113
+ }
114
+
115
+ async function doPublishSkill(config: AppConfig, skillDir: string): Promise<void> {
116
+ const fs = await import("fs/promises")
117
+ const path = await import("path")
118
+
119
+ const mdPath = path.join(skillDir, "SKILL.md")
120
+ let content: string
121
+ try {
122
+ content = await fs.readFile(mdPath, "utf-8")
123
+ } catch {
124
+ console.log(chalk.red(`No SKILL.md found in: ${skillDir}`))
125
+ return
126
+ }
127
+
128
+ const name = path.basename(path.resolve(skillDir))
129
+ const description = content.split("\n").find((l) => l.trim() && !l.trim().startsWith("#") && !l.trim().startsWith("---"))?.trim() ?? `Skill: ${name}`
130
+
131
+ console.log(chalk.cyan(`\nPublishing skill: ${name}`))
132
+ console.log(chalk.dim(`Path: ${skillDir}`))
133
+ console.log(chalk.dim(`Description: ${description.slice(0, 80)}`))
134
+ console.log()
135
+
136
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
137
+ const answer = await new Promise<string>((resolve) => {
138
+ rl.question(chalk.yellow("Publish this skill? [y/N] "), (ans) => {
139
+ rl.close()
140
+ resolve(ans.trim().toLowerCase())
141
+ })
142
+ })
143
+
144
+ if (answer !== "y" && answer !== "yes") {
145
+ console.log(chalk.dim("Cancelled."))
146
+ return
147
+ }
148
+
149
+ try {
150
+ const result = await publishSkill(config, {
151
+ name,
152
+ description,
153
+ content,
154
+ trustLevel: "community",
155
+ allowedTools: [],
156
+ })
157
+
158
+ if (result.published) {
159
+ console.log(chalk.green(`\nSkill "${result.skillName}" published successfully!`))
160
+ console.log(chalk.dim("It will appear in the marketplace after review."))
161
+ } else {
162
+ console.log(chalk.red(`\nFailed to publish skill "${name}".`))
163
+ }
164
+ } catch (err: unknown) {
165
+ const msg = err instanceof Error ? err.message : String(err)
166
+ console.log(chalk.red(`\nFailed to publish: ${msg}`))
167
+ }
168
+ }
169
+
170
+ export async function listSkillsCommand(): Promise<void> {
171
+ await marketplaceCommand("list", [])
172
+ }
173
+
174
+ export async function installSkillCommand(name: string): Promise<void> {
175
+ await marketplaceCommand("install", [name])
176
+ }
177
+
178
+ export async function publishSkillCommand(dir: string): Promise<void> {
179
+ await marketplaceCommand("publish", [dir])
180
+ }
181
+
182
+ export async function signSkillCommand(dir: string): Promise<void> {
183
+ const { signSkill, generateKeyPair } = await import("../skills/signing/signer")
184
+ const keys = generateKeyPair()
185
+ const result = signSkill(dir, keys.privateKey, keys.publicKey)
186
+ console.log(chalk.green(`\nSkill signed: ${dir}`))
187
+ console.log(chalk.dim(`Signature: ${result.signature}`))
188
+ console.log(chalk.dim(`Public key: ${result.publicKey}`))
189
+ console.log(chalk.yellow("\nSave your private key securely. You'll need it to sign future versions."))
190
+ }
@@ -0,0 +1,74 @@
1
+ import chalk from "chalk";
2
+ import Table from "cli-table3";
3
+ import { loadConfig, getApiBase, getApiKey } from "../auth/config";
4
+
5
+ export async function modelsCommand(): Promise<void> {
6
+ const apiKey = getApiKey();
7
+ if (!apiKey) {
8
+ console.log(chalk.red('Not authenticated. Run "llmtune login" first.'));
9
+ process.exit(1);
10
+ }
11
+
12
+ const apiBase = getApiBase();
13
+
14
+ try {
15
+ const modelsUrl = apiBase.replace(/\/$/, "") + "/models";
16
+ const response = await fetch(modelsUrl, {
17
+ headers: {
18
+ Authorization: `Bearer ${apiKey}`,
19
+ },
20
+ });
21
+
22
+ if (!response.ok) {
23
+ const body = await response.text();
24
+ console.log(chalk.red(`Failed to fetch models: ${response.status}`));
25
+ console.log(chalk.dim(body.slice(0, 200)));
26
+ process.exit(1);
27
+ }
28
+
29
+ const data = (await response.json()) as {
30
+ data: Array<{
31
+ id: string;
32
+ object: string;
33
+ created: number;
34
+ owned_by: string;
35
+ }>;
36
+ subscription?: {
37
+ planName: string;
38
+ planModels: string[];
39
+ quotaDaily: number;
40
+ };
41
+ };
42
+
43
+ if (data.subscription) {
44
+ console.log(
45
+ chalk.cyan(`\nSubscription: ${data.subscription.planName}`)
46
+ );
47
+ console.log(
48
+ chalk.dim(
49
+ `Daily quota: ${data.subscription.quotaDaily} requests\n`
50
+ )
51
+ );
52
+ }
53
+
54
+ const table = new Table({
55
+ head: [
56
+ chalk.cyan("Model ID"),
57
+ chalk.cyan("Provider"),
58
+ ],
59
+ colWidths: [50, 20],
60
+ });
61
+
62
+ for (const model of data.data) {
63
+ table.push([model.id, model.owned_by]);
64
+ }
65
+
66
+ console.log(table.toString());
67
+ console.log(chalk.dim(`\n${data.data.length} models available`));
68
+ } catch (err) {
69
+ console.log(
70
+ chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`)
71
+ );
72
+ process.exit(1);
73
+ }
74
+ }
@@ -0,0 +1,101 @@
1
+ import * as fs from "fs"
2
+ import * as path from "path"
3
+ import * as os from "os"
4
+ import type { Message } from "../agent/conversation"
5
+
6
+ export interface HistorySnapshot {
7
+ sessionId: string
8
+ timestamp: string
9
+ reason: "manual" | "auto"
10
+ messageCount: number
11
+ tokenEstimate: number
12
+ messages: Message[]
13
+ }
14
+
15
+ const SESSIONS_DIR = () => {
16
+ const dir = path.join(os.homedir(), ".llmtune", "sessions")
17
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
18
+ return dir
19
+ }
20
+
21
+ export function getHistoryDir(sessionId: string): string {
22
+ const dir = path.join(SESSIONS_DIR(), sessionId)
23
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
24
+ return dir
25
+ }
26
+
27
+ export function saveRawHistory(
28
+ sessionId: string,
29
+ messages: Message[],
30
+ reason: "manual" | "auto" = "manual"
31
+ ): string {
32
+ const dir = getHistoryDir(sessionId)
33
+ const filePath = path.join(dir, "history.json")
34
+
35
+ const tokenEstimate = messages.reduce((total, msg) => {
36
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
37
+ return total + Math.ceil(content.length / 4)
38
+ }, 0)
39
+
40
+ const snapshot: HistorySnapshot = {
41
+ sessionId,
42
+ timestamp: new Date().toISOString(),
43
+ reason,
44
+ messageCount: messages.length,
45
+ tokenEstimate,
46
+ messages,
47
+ }
48
+
49
+ fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2), "utf-8")
50
+ return filePath
51
+ }
52
+
53
+ export function loadRawHistory(sessionId: string): HistorySnapshot | null {
54
+ const filePath = path.join(getHistoryDir(sessionId), "history.json")
55
+ if (!fs.existsSync(filePath)) return null
56
+ try {
57
+ return JSON.parse(fs.readFileSync(filePath, "utf-8")) as HistorySnapshot
58
+ } catch {
59
+ return null
60
+ }
61
+ }
62
+
63
+ export function saveCompactionMeta(
64
+ sessionId: string,
65
+ meta: {
66
+ compactedAt: string
67
+ tokensBefore: number
68
+ tokensAfter: number
69
+ tokensSaved: number
70
+ messagesBefore: number
71
+ messagesAfter: number
72
+ }
73
+ ): void {
74
+ const filePath = path.join(getHistoryDir(sessionId), "compaction-meta.json")
75
+ const history: typeof meta[] = []
76
+ if (fs.existsSync(filePath)) {
77
+ try {
78
+ const existing = JSON.parse(fs.readFileSync(filePath, "utf-8"))
79
+ if (Array.isArray(existing)) history.push(...existing)
80
+ } catch { /* ignore */ }
81
+ }
82
+ history.push(meta)
83
+ fs.writeFileSync(filePath, JSON.stringify(history, null, 2), "utf-8")
84
+ }
85
+
86
+ export function loadCompactionHistory(sessionId: string): Array<{
87
+ compactedAt: string
88
+ tokensBefore: number
89
+ tokensAfter: number
90
+ tokensSaved: number
91
+ messagesBefore: number
92
+ messagesAfter: number
93
+ }> {
94
+ const filePath = path.join(getHistoryDir(sessionId), "compaction-meta.json")
95
+ if (!fs.existsSync(filePath)) return []
96
+ try {
97
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"))
98
+ } catch {
99
+ return []
100
+ }
101
+ }
@@ -0,0 +1,49 @@
1
+ import type { Message } from "../agent/conversation"
2
+
3
+ /**
4
+ * Microcompact: reduce token usage by stripping verbose tool results
5
+ * and compressing old messages. Ported from Clawd-Code compact_service/microcompact.py.
6
+ */
7
+
8
+ export function microcompactMessages(messages: Message[]): {
9
+ compacted: Message[]
10
+ tokensSaved: number
11
+ } {
12
+ let tokensSaved = 0
13
+ const compacted: Message[] = []
14
+
15
+ for (const msg of messages) {
16
+ if (msg.role === "tool") {
17
+ const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
18
+ if (text.length > 2000) {
19
+ const compressed = compressToolResult(text)
20
+ tokensSaved += Math.ceil((text.length - compressed.length) / 4)
21
+ compacted.push({ ...msg, content: compressed })
22
+ continue
23
+ }
24
+ }
25
+
26
+ if (msg.role === "assistant" && msg.content) {
27
+ const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
28
+ if (text.length > 10000) {
29
+ const truncated = text.slice(0, 5000) + "\n... [truncated for compaction]"
30
+ tokensSaved += Math.ceil((text.length - truncated.length) / 4)
31
+ compacted.push({ ...msg, content: truncated })
32
+ continue
33
+ }
34
+ }
35
+
36
+ compacted.push(msg)
37
+ }
38
+
39
+ return { compacted, tokensSaved }
40
+ }
41
+
42
+ function compressToolResult(text: string): string {
43
+ // Keep first 500 chars and last 500 chars of large tool results
44
+ if (text.length <= 2000) return text
45
+ const head = text.slice(0, 500)
46
+ const tail = text.slice(-500)
47
+ const lines = text.split("\n").length
48
+ return `${head}\n\n... [${lines} lines compressed, ${text.length} chars total] ...\n\n${tail}`
49
+ }