@llmtune/cli 0.1.0 → 0.1.1

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 (196) 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 +49 -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 +1 -1
  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 +374 -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 +142 -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/package.json +2 -2
  99. package/dist/agent/conversation.d.ts.map +0 -1
  100. package/dist/agent/loop.d.ts.map +0 -1
  101. package/dist/agent/planner.d.ts.map +0 -1
  102. package/dist/auth/client.d.ts.map +0 -1
  103. package/dist/auth/config.d.ts.map +0 -1
  104. package/dist/commands/chat.d.ts.map +0 -1
  105. package/dist/commands/config.d.ts.map +0 -1
  106. package/dist/commands/login.d.ts.map +0 -1
  107. package/dist/commands/marketplace.d.ts.map +0 -1
  108. package/dist/commands/models.d.ts.map +0 -1
  109. package/dist/compact/history-store.d.ts.map +0 -1
  110. package/dist/compact/microcompact.d.ts.map +0 -1
  111. package/dist/compact/service.d.ts.map +0 -1
  112. package/dist/context/analyzer.d.ts.map +0 -1
  113. package/dist/context/builder.d.ts.map +0 -1
  114. package/dist/context/cache.d.ts.map +0 -1
  115. package/dist/context/git-context.d.ts.map +0 -1
  116. package/dist/context/llmtune-md.d.ts.map +0 -1
  117. package/dist/context/workspace.d.ts.map +0 -1
  118. package/dist/index.d.ts.map +0 -1
  119. package/dist/marketplace/client.d.ts.map +0 -1
  120. package/dist/memory/files.d.ts.map +0 -1
  121. package/dist/memory/service.d.ts.map +0 -1
  122. package/dist/repl/repl.d.ts.map +0 -1
  123. package/dist/skills/args.d.ts.map +0 -1
  124. package/dist/skills/frontmatter.d.ts.map +0 -1
  125. package/dist/skills/loader.d.ts.map +0 -1
  126. package/dist/skills/registry.d.ts.map +0 -1
  127. package/dist/skills/signing/signer.d.ts.map +0 -1
  128. package/dist/skills/trust.d.ts.map +0 -1
  129. package/dist/telemetry/logger.d.ts.map +0 -1
  130. package/dist/tools/permissions.d.ts.map +0 -1
  131. package/dist/tools/protocol.d.ts.map +0 -1
  132. package/dist/tools/registry.d.ts.map +0 -1
  133. package/dist/tools/sandbox/docker.d.ts.map +0 -1
  134. package/dist/tools/sandbox/index.d.ts.map +0 -1
  135. package/dist/tools/tools/ask-user.d.ts.map +0 -1
  136. package/dist/tools/tools/bash.d.ts.map +0 -1
  137. package/dist/tools/tools/edit.d.ts.map +0 -1
  138. package/dist/tools/tools/glob.d.ts.map +0 -1
  139. package/dist/tools/tools/grep.d.ts.map +0 -1
  140. package/dist/tools/tools/read.d.ts.map +0 -1
  141. package/dist/tools/tools/web-fetch.d.ts.map +0 -1
  142. package/dist/tools/tools/write.d.ts.map +0 -1
  143. package/dist/tools/validation.d.ts.map +0 -1
  144. package/dist/utils/markdown.d.ts.map +0 -1
  145. package/dist/utils/streaming.d.ts.map +0 -1
  146. package/dist/utils/tokens.d.ts.map +0 -1
  147. package/src/agent/conversation.ts +0 -140
  148. package/src/agent/loop.ts +0 -215
  149. package/src/agent/planner.ts +0 -55
  150. package/src/auth/client.ts +0 -19
  151. package/src/auth/config.ts +0 -89
  152. package/src/commands/chat.ts +0 -28
  153. package/src/commands/config.ts +0 -36
  154. package/src/commands/login.ts +0 -63
  155. package/src/commands/marketplace.ts +0 -190
  156. package/src/commands/models.ts +0 -74
  157. package/src/compact/history-store.ts +0 -101
  158. package/src/compact/microcompact.ts +0 -49
  159. package/src/compact/service.ts +0 -154
  160. package/src/context/analyzer.ts +0 -127
  161. package/src/context/builder.ts +0 -123
  162. package/src/context/cache.ts +0 -11
  163. package/src/context/git-context.ts +0 -58
  164. package/src/context/llmtune-md.ts +0 -48
  165. package/src/context/workspace.ts +0 -139
  166. package/src/index.ts +0 -100
  167. package/src/marketplace/client.ts +0 -118
  168. package/src/memory/files.ts +0 -81
  169. package/src/memory/service.ts +0 -124
  170. package/src/repl/repl.ts +0 -400
  171. package/src/skills/args.ts +0 -35
  172. package/src/skills/builtin/explain-code/SKILL.md +0 -30
  173. package/src/skills/frontmatter.ts +0 -47
  174. package/src/skills/loader.ts +0 -25
  175. package/src/skills/registry.ts +0 -155
  176. package/src/skills/signing/signer.ts +0 -101
  177. package/src/skills/trust.ts +0 -50
  178. package/src/telemetry/logger.ts +0 -108
  179. package/src/tools/permissions.ts +0 -83
  180. package/src/tools/protocol.ts +0 -24
  181. package/src/tools/registry.ts +0 -93
  182. package/src/tools/sandbox/docker.ts +0 -225
  183. package/src/tools/sandbox/index.ts +0 -91
  184. package/src/tools/tools/ask-user.ts +0 -60
  185. package/src/tools/tools/bash.ts +0 -97
  186. package/src/tools/tools/edit.ts +0 -111
  187. package/src/tools/tools/glob.ts +0 -68
  188. package/src/tools/tools/grep.ts +0 -121
  189. package/src/tools/tools/read.ts +0 -57
  190. package/src/tools/tools/web-fetch.ts +0 -158
  191. package/src/tools/tools/write.ts +0 -52
  192. package/src/tools/validation.ts +0 -164
  193. package/src/utils/markdown.ts +0 -96
  194. package/src/utils/streaming.ts +0 -63
  195. package/src/utils/tokens.ts +0 -41
  196. package/tsconfig.json +0 -20
@@ -1,190 +0,0 @@
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
- }
@@ -1,74 +0,0 @@
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
- }
@@ -1,101 +0,0 @@
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
- }
@@ -1,49 +0,0 @@
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
- }
@@ -1,154 +0,0 @@
1
- import OpenAI from "openai"
2
- import { Conversation, type Message } from "../agent/conversation"
3
- import { estimateTokens } from "../utils/tokens"
4
- import * as fs from "fs"
5
- import * as path from "path"
6
- import * as os from "os"
7
-
8
- export interface CompactResult {
9
- tokensSaved: number
10
- preCompactTokens: number
11
- postCompactTokens: number
12
- preCompactMessages: number
13
- postCompactMessages: number
14
- summary: string
15
- }
16
-
17
- const COMPACT_SYSTEM_PROMPT = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
18
-
19
- Your task is to create a detailed summary of the conversation so far.
20
-
21
- Include:
22
- 1. Primary Request and Intent: What the user asked for
23
- 2. Key Technical Concepts: Technologies, frameworks, patterns discussed
24
- 3. Files and Code Sections: Files examined, modified, or created (with key snippets)
25
- 4. Errors and Fixes: Problems encountered and how they were resolved
26
- 5. Problem Solving: What was accomplished
27
- 6. All User Messages: Summarize all non-tool-result user messages
28
- 7. Pending Tasks: Tasks explicitly requested but not yet done
29
- 8. Current Work: What was being worked on immediately before this summary
30
-
31
- Respond ONLY with plain text. No XML tags. No tool calls.`
32
-
33
- export async function compactConversation(
34
- client: OpenAI,
35
- model: string,
36
- conversation: Conversation,
37
- sessionsDir?: string,
38
- ): Promise<CompactResult> {
39
- const messages = conversation.messages
40
- const preCompactTokens = estimateTokens(
41
- messages.map((m) => (typeof m.content === "string" ? m.content : JSON.stringify(m.content))).join(" "),
42
- )
43
- const preCompactCount = messages.length
44
-
45
- // Save raw history before compacting
46
- saveRawHistory(conversation, sessionsDir)
47
-
48
- // Build context for summary: last N messages
49
- const contextMessages = messages.slice(-20)
50
- const summaryRequestMessages = contextMessages
51
- .filter((m) => m.role !== "system")
52
- .map(
53
- (m): OpenAI.ChatCompletionMessageParam => ({
54
- role: m.role as "user" | "assistant",
55
- content: typeof m.content === "string" ? m.content : JSON.stringify(m.content),
56
- }),
57
- )
58
- summaryRequestMessages.push({ role: "user", content: COMPACT_SYSTEM_PROMPT })
59
-
60
- let summary = ""
61
- try {
62
- const response = await client.chat.completions.create({
63
- model,
64
- messages: summaryRequestMessages,
65
- max_tokens: 4096,
66
- temperature: 0,
67
- })
68
- summary = response.choices[0]?.message?.content?.trim() ?? ""
69
- } catch {
70
- summary = buildFallbackSummary(messages)
71
- }
72
-
73
- if (!summary) {
74
- summary = buildFallbackSummary(messages)
75
- }
76
-
77
- // Replace conversation with boundary marker + summary
78
- const boundaryMsg: Message = {
79
- role: "system",
80
- content: `[COMPACT BOUNDARY] Compacted at ${new Date().toISOString()}. ${preCompactCount} messages summarized. Raw history preserved.`,
81
- }
82
- const summaryMsg: Message = {
83
- role: "system",
84
- content: `## Conversation Summary\n\n${summary}`,
85
- }
86
-
87
- // Keep system messages + boundary + summary
88
- const systemMessages = messages.filter((m) => m.role === "system" && m === messages[0])
89
- conversation.messages.length = 0
90
- conversation.messages.push(...systemMessages, boundaryMsg, summaryMsg)
91
-
92
- const postCompactTokens = estimateTokens(
93
- conversation.messages.map((m) => (typeof m.content === "string" ? m.content : JSON.stringify(m.content))).join(" "),
94
- )
95
- const postCompactCount = conversation.messages.length
96
-
97
- return {
98
- tokensSaved: Math.max(0, preCompactTokens - postCompactTokens),
99
- preCompactTokens,
100
- postCompactTokens,
101
- preCompactMessages: preCompactCount,
102
- postCompactMessages: postCompactCount,
103
- summary,
104
- }
105
- }
106
-
107
- export function uncompactConversation(conversation: Conversation, sessionsDir?: string): boolean {
108
- const dir = sessionsDir ?? path.join(os.homedir(), ".llmtune", "sessions")
109
- const rawPath = path.join(dir, `${conversation.id}.raw.json`)
110
-
111
- if (!fs.existsSync(rawPath)) {
112
- return false
113
- }
114
-
115
- try {
116
- const raw = JSON.parse(fs.readFileSync(rawPath, "utf-8")) as { messages: Message[] }
117
- conversation.messages.length = 0
118
- conversation.messages.push(...raw.messages)
119
- return true
120
- } catch {
121
- return false
122
- }
123
- }
124
-
125
- function saveRawHistory(conversation: Conversation, sessionsDir?: string): void {
126
- const dir = sessionsDir ?? path.join(os.homedir(), ".llmtune", "sessions")
127
- if (!fs.existsSync(dir)) {
128
- fs.mkdirSync(dir, { recursive: true })
129
- }
130
- const rawPath = path.join(dir, `${conversation.id}.raw.json`)
131
- if (!fs.existsSync(rawPath)) {
132
- fs.writeFileSync(rawPath, JSON.stringify({ messages: conversation.messages }, null, 2), "utf-8")
133
- }
134
- }
135
-
136
- function buildFallbackSummary(messages: Message[]): string {
137
- const userMsgs = messages
138
- .filter((m) => m.role === "user")
139
- .map((m) => (typeof m.content === "string" ? m.content.slice(0, 200) : ""))
140
- .filter(Boolean)
141
- const toolNames = messages
142
- .filter((m) => m.role === "assistant" && m.toolCalls)
143
- .flatMap((m) => m.toolCalls?.map((tc) => tc.function.name) ?? [])
144
-
145
- const parts = [`Conversation had ${messages.length} messages.`]
146
- if (toolNames.length > 0) {
147
- const unique = [...new Set(toolNames)]
148
- parts.push(`Tools used: ${unique.join(", ")}`)
149
- }
150
- if (userMsgs.length > 0) {
151
- parts.push(`Last user message: ${userMsgs[userMsgs.length - 1].slice(0, 150)}`)
152
- }
153
- return parts.join("\n")
154
- }
@@ -1,127 +0,0 @@
1
- import type { ToolSpec } from "../tools/protocol"
2
- import { estimateTokens } from "../utils/tokens"
3
-
4
- export interface ContextCategory {
5
- name: string
6
- tokens: number
7
- }
8
-
9
- export interface ContextAnalysis {
10
- categories: ContextCategory[]
11
- totalTokens: number
12
- maxTokens: number
13
- percentage: number
14
- model: string
15
- }
16
-
17
- const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
18
- "gpt-4o": 128_000,
19
- "gpt-4o-mini": 128_000,
20
- "gpt-4-turbo": 128_000,
21
- "glm-5.1": 128_000,
22
- "glm-5-turbo": 128_000,
23
- "glm-4.7-flash": 128_000,
24
- "claude-sonnet": 200_000,
25
- "claude-opus": 200_000,
26
- "deepseek-r1": 128_000,
27
- "qwen3": 128_000,
28
- }
29
-
30
- const DEFAULT_CONTEXT_WINDOW = 128_000
31
-
32
- export function getContextWindowForModel(model: string): number {
33
- const lower = model.toLowerCase()
34
- for (const [name, window] of Object.entries(MODEL_CONTEXT_WINDOWS)) {
35
- if (lower.includes(name)) return window
36
- }
37
- const kMatch = lower.match(/(\d+)k/)
38
- if (kMatch) return parseInt(kMatch[1], 10) * 1_000
39
- if (lower.includes("1m") || lower.includes("million")) return 1_000_000
40
- return DEFAULT_CONTEXT_WINDOW
41
- }
42
-
43
- export function analyzeContextUsage(options: {
44
- systemPrompt: string
45
- toolSpecs: ToolSpec[]
46
- messages: Array<{ role: string; content: unknown }>
47
- skillsContent?: string
48
- memoryContent?: string
49
- model: string
50
- }): ContextAnalysis {
51
- const categories: ContextCategory[] = []
52
-
53
- const systemTokens = estimateTokens(options.systemPrompt)
54
- if (systemTokens > 0) {
55
- categories.push({ name: "System prompt", tokens: systemTokens })
56
- }
57
-
58
- let toolTokens = 0
59
- for (const spec of options.toolSpecs) {
60
- toolTokens += estimateTokens(spec.description)
61
- toolTokens += estimateTokens(JSON.stringify(spec.inputSchema))
62
- }
63
- if (toolTokens > 0) {
64
- categories.push({ name: "Tool definitions", tokens: toolTokens })
65
- }
66
-
67
- if (options.skillsContent) {
68
- const skillsTokens = estimateTokens(options.skillsContent)
69
- if (skillsTokens > 0) {
70
- categories.push({ name: "Skills", tokens: skillsTokens })
71
- }
72
- }
73
-
74
- if (options.memoryContent) {
75
- const memTokens = estimateTokens(options.memoryContent)
76
- if (memTokens > 0) {
77
- categories.push({ name: "Memory", tokens: memTokens })
78
- }
79
- }
80
-
81
- let messageTokens = 0
82
- for (const msg of options.messages) {
83
- const text = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content ?? "")
84
- messageTokens += estimateTokens(text)
85
- }
86
- if (messageTokens > 0) {
87
- categories.push({ name: "Messages", tokens: messageTokens })
88
- }
89
-
90
- const totalTokens = categories.reduce((sum, c) => sum + c.tokens, 0)
91
- const maxTokens = getContextWindowForModel(options.model)
92
- const percentage = maxTokens > 0 ? Math.round((totalTokens / maxTokens) * 1000) / 10 : 0
93
-
94
- const freeTokens = Math.max(0, maxTokens - totalTokens)
95
- categories.push({ name: "Free space", tokens: freeTokens })
96
-
97
- return { categories, totalTokens, maxTokens, percentage, model: options.model }
98
- }
99
-
100
- export function formatContextAnalysis(analysis: ContextAnalysis): string {
101
- const lines: string[] = [
102
- "## Context Usage",
103
- "",
104
- `**Model:** ${analysis.model}`,
105
- `**Tokens:** ${analysis.totalTokens.toLocaleString()} / ${analysis.maxTokens.toLocaleString()} (${analysis.percentage}%)`,
106
- "",
107
- ]
108
-
109
- const visible = analysis.categories.filter((c) => c.tokens > 0 && c.name !== "Free space")
110
-
111
- if (visible.length > 0) {
112
- lines.push("| Category | Tokens | Percentage |")
113
- lines.push("|----------|--------|------------|")
114
- for (const cat of visible) {
115
- const pct = analysis.maxTokens > 0 ? ((cat.tokens / analysis.maxTokens) * 100).toFixed(1) : "0.0"
116
- lines.push(`| ${cat.name} | ${cat.tokens.toLocaleString()} | ${pct}% |`)
117
- }
118
- const free = analysis.categories.find((c) => c.name === "Free space")
119
- if (free && free.tokens > 0) {
120
- const pct = analysis.maxTokens > 0 ? ((free.tokens / analysis.maxTokens) * 100).toFixed(1) : "0.0"
121
- lines.push(`| Free space | ${free.tokens.toLocaleString()} | ${pct}% |`)
122
- }
123
- lines.push("")
124
- }
125
-
126
- return lines.join("\n")
127
- }