@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.
- package/README.md +1 -1
- package/dist/agent/conversation.d.ts +42 -0
- package/dist/agent/conversation.js +105 -0
- package/dist/agent/loop.d.ts +19 -0
- package/dist/agent/loop.js +185 -0
- package/dist/agent/planner.d.ts +8 -0
- package/dist/agent/planner.js +43 -0
- package/dist/auth/client.d.ts +4 -0
- package/dist/auth/client.js +24 -0
- package/dist/auth/config.d.ts +21 -0
- package/dist/auth/config.js +83 -0
- package/dist/commands/chat.d.ts +5 -0
- package/dist/commands/chat.js +27 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +37 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +93 -0
- package/dist/commands/marketplace.d.ts +6 -0
- package/dist/commands/marketplace.js +213 -0
- package/dist/commands/models.d.ts +2 -0
- package/dist/commands/models.js +53 -0
- package/dist/compact/history-store.d.ts +29 -0
- package/dist/compact/history-store.js +110 -0
- package/dist/compact/microcompact.d.ts +10 -0
- package/dist/compact/microcompact.js +43 -0
- package/dist/compact/service.d.ts +13 -0
- package/dist/compact/service.js +156 -0
- package/dist/context/analyzer.d.ts +26 -0
- package/dist/context/analyzer.js +99 -0
- package/dist/context/builder.d.ts +13 -0
- package/dist/context/builder.js +144 -0
- package/dist/context/cache.d.ts +6 -0
- package/dist/context/cache.js +8 -0
- package/dist/context/git-context.d.ts +9 -0
- package/dist/context/git-context.js +49 -0
- package/dist/context/llmtune-md.d.ts +6 -0
- package/dist/context/llmtune-md.js +73 -0
- package/dist/context/workspace.d.ts +11 -0
- package/dist/context/workspace.js +115 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -1
- package/dist/marketplace/client.d.ts +52 -0
- package/dist/marketplace/client.js +86 -0
- package/dist/memory/files.d.ts +14 -0
- package/dist/memory/files.js +116 -0
- package/dist/memory/service.d.ts +22 -0
- package/dist/memory/service.js +146 -0
- package/dist/repl/repl.d.ts +8 -0
- package/dist/repl/repl.js +374 -0
- package/dist/skills/args.d.ts +10 -0
- package/dist/skills/args.js +37 -0
- package/dist/skills/frontmatter.d.ts +6 -0
- package/dist/skills/frontmatter.js +44 -0
- package/dist/skills/loader.d.ts +5 -0
- package/dist/skills/loader.js +59 -0
- package/dist/skills/registry.d.ts +27 -0
- package/dist/skills/registry.js +162 -0
- package/dist/skills/signing/signer.d.ts +19 -0
- package/dist/skills/signing/signer.js +110 -0
- package/dist/skills/trust.d.ts +11 -0
- package/dist/skills/trust.js +42 -0
- package/dist/telemetry/logger.d.ts +51 -0
- package/dist/telemetry/logger.js +135 -0
- package/dist/tools/permissions.d.ts +20 -0
- package/dist/tools/permissions.js +58 -0
- package/dist/tools/protocol.d.ts +22 -0
- package/dist/tools/protocol.js +3 -0
- package/dist/tools/registry.d.ts +20 -0
- package/dist/tools/registry.js +77 -0
- package/dist/tools/sandbox/docker.d.ts +16 -0
- package/dist/tools/sandbox/docker.js +240 -0
- package/dist/tools/sandbox/index.d.ts +18 -0
- package/dist/tools/sandbox/index.js +80 -0
- package/dist/tools/tools/ask-user.d.ts +3 -0
- package/dist/tools/tools/ask-user.js +56 -0
- package/dist/tools/tools/bash.d.ts +3 -0
- package/dist/tools/tools/bash.js +85 -0
- package/dist/tools/tools/edit.d.ts +3 -0
- package/dist/tools/tools/edit.js +138 -0
- package/dist/tools/tools/glob.d.ts +3 -0
- package/dist/tools/tools/glob.js +63 -0
- package/dist/tools/tools/grep.d.ts +3 -0
- package/dist/tools/tools/grep.js +148 -0
- package/dist/tools/tools/read.d.ts +3 -0
- package/dist/tools/tools/read.js +85 -0
- package/dist/tools/tools/web-fetch.d.ts +3 -0
- package/dist/tools/tools/web-fetch.js +142 -0
- package/dist/tools/tools/write.d.ts +3 -0
- package/dist/tools/tools/write.js +84 -0
- package/dist/tools/validation.d.ts +13 -0
- package/dist/tools/validation.js +142 -0
- package/dist/utils/markdown.d.ts +9 -0
- package/dist/utils/markdown.js +89 -0
- package/dist/utils/streaming.d.ts +10 -0
- package/dist/utils/streaming.js +63 -0
- package/dist/utils/tokens.d.ts +12 -0
- package/dist/utils/tokens.js +44 -0
- package/package.json +2 -2
- package/dist/agent/conversation.d.ts.map +0 -1
- package/dist/agent/loop.d.ts.map +0 -1
- package/dist/agent/planner.d.ts.map +0 -1
- package/dist/auth/client.d.ts.map +0 -1
- package/dist/auth/config.d.ts.map +0 -1
- package/dist/commands/chat.d.ts.map +0 -1
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/marketplace.d.ts.map +0 -1
- package/dist/commands/models.d.ts.map +0 -1
- package/dist/compact/history-store.d.ts.map +0 -1
- package/dist/compact/microcompact.d.ts.map +0 -1
- package/dist/compact/service.d.ts.map +0 -1
- package/dist/context/analyzer.d.ts.map +0 -1
- package/dist/context/builder.d.ts.map +0 -1
- package/dist/context/cache.d.ts.map +0 -1
- package/dist/context/git-context.d.ts.map +0 -1
- package/dist/context/llmtune-md.d.ts.map +0 -1
- package/dist/context/workspace.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/marketplace/client.d.ts.map +0 -1
- package/dist/memory/files.d.ts.map +0 -1
- package/dist/memory/service.d.ts.map +0 -1
- package/dist/repl/repl.d.ts.map +0 -1
- package/dist/skills/args.d.ts.map +0 -1
- package/dist/skills/frontmatter.d.ts.map +0 -1
- package/dist/skills/loader.d.ts.map +0 -1
- package/dist/skills/registry.d.ts.map +0 -1
- package/dist/skills/signing/signer.d.ts.map +0 -1
- package/dist/skills/trust.d.ts.map +0 -1
- package/dist/telemetry/logger.d.ts.map +0 -1
- package/dist/tools/permissions.d.ts.map +0 -1
- package/dist/tools/protocol.d.ts.map +0 -1
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/sandbox/docker.d.ts.map +0 -1
- package/dist/tools/sandbox/index.d.ts.map +0 -1
- package/dist/tools/tools/ask-user.d.ts.map +0 -1
- package/dist/tools/tools/bash.d.ts.map +0 -1
- package/dist/tools/tools/edit.d.ts.map +0 -1
- package/dist/tools/tools/glob.d.ts.map +0 -1
- package/dist/tools/tools/grep.d.ts.map +0 -1
- package/dist/tools/tools/read.d.ts.map +0 -1
- package/dist/tools/tools/web-fetch.d.ts.map +0 -1
- package/dist/tools/tools/write.d.ts.map +0 -1
- package/dist/tools/validation.d.ts.map +0 -1
- package/dist/utils/markdown.d.ts.map +0 -1
- package/dist/utils/streaming.d.ts.map +0 -1
- package/dist/utils/tokens.d.ts.map +0 -1
- package/src/agent/conversation.ts +0 -140
- package/src/agent/loop.ts +0 -215
- package/src/agent/planner.ts +0 -55
- package/src/auth/client.ts +0 -19
- package/src/auth/config.ts +0 -89
- package/src/commands/chat.ts +0 -28
- package/src/commands/config.ts +0 -36
- package/src/commands/login.ts +0 -63
- package/src/commands/marketplace.ts +0 -190
- package/src/commands/models.ts +0 -74
- package/src/compact/history-store.ts +0 -101
- package/src/compact/microcompact.ts +0 -49
- package/src/compact/service.ts +0 -154
- package/src/context/analyzer.ts +0 -127
- package/src/context/builder.ts +0 -123
- package/src/context/cache.ts +0 -11
- package/src/context/git-context.ts +0 -58
- package/src/context/llmtune-md.ts +0 -48
- package/src/context/workspace.ts +0 -139
- package/src/index.ts +0 -100
- package/src/marketplace/client.ts +0 -118
- package/src/memory/files.ts +0 -81
- package/src/memory/service.ts +0 -124
- package/src/repl/repl.ts +0 -400
- package/src/skills/args.ts +0 -35
- package/src/skills/builtin/explain-code/SKILL.md +0 -30
- package/src/skills/frontmatter.ts +0 -47
- package/src/skills/loader.ts +0 -25
- package/src/skills/registry.ts +0 -155
- package/src/skills/signing/signer.ts +0 -101
- package/src/skills/trust.ts +0 -50
- package/src/telemetry/logger.ts +0 -108
- package/src/tools/permissions.ts +0 -83
- package/src/tools/protocol.ts +0 -24
- package/src/tools/registry.ts +0 -93
- package/src/tools/sandbox/docker.ts +0 -225
- package/src/tools/sandbox/index.ts +0 -91
- package/src/tools/tools/ask-user.ts +0 -60
- package/src/tools/tools/bash.ts +0 -97
- package/src/tools/tools/edit.ts +0 -111
- package/src/tools/tools/glob.ts +0 -68
- package/src/tools/tools/grep.ts +0 -121
- package/src/tools/tools/read.ts +0 -57
- package/src/tools/tools/web-fetch.ts +0 -158
- package/src/tools/tools/write.ts +0 -52
- package/src/tools/validation.ts +0 -164
- package/src/utils/markdown.ts +0 -96
- package/src/utils/streaming.ts +0 -63
- package/src/utils/tokens.ts +0 -41
- package/tsconfig.json +0 -20
|
@@ -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
|
-
}
|
package/src/commands/models.ts
DELETED
|
@@ -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
|
-
}
|
package/src/compact/service.ts
DELETED
|
@@ -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
|
-
}
|
package/src/context/analyzer.ts
DELETED
|
@@ -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
|
-
}
|