@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.
- package/README.md +181 -0
- package/dist/agent/conversation.d.ts.map +1 -0
- package/dist/agent/loop.d.ts.map +1 -0
- package/dist/agent/planner.d.ts.map +1 -0
- package/dist/auth/client.d.ts.map +1 -0
- package/dist/auth/config.d.ts.map +1 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/marketplace.d.ts.map +1 -0
- package/dist/commands/models.d.ts.map +1 -0
- package/dist/compact/history-store.d.ts.map +1 -0
- package/dist/compact/microcompact.d.ts.map +1 -0
- package/dist/compact/service.d.ts.map +1 -0
- package/dist/context/analyzer.d.ts.map +1 -0
- package/dist/context/builder.d.ts.map +1 -0
- package/dist/context/cache.d.ts.map +1 -0
- package/dist/context/git-context.d.ts.map +1 -0
- package/dist/context/llmtune-md.d.ts.map +1 -0
- package/dist/context/workspace.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +125 -0
- package/dist/marketplace/client.d.ts.map +1 -0
- package/dist/memory/files.d.ts.map +1 -0
- package/dist/memory/service.d.ts.map +1 -0
- package/dist/repl/repl.d.ts.map +1 -0
- package/dist/skills/args.d.ts.map +1 -0
- package/dist/skills/frontmatter.d.ts.map +1 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/registry.d.ts.map +1 -0
- package/dist/skills/signing/signer.d.ts.map +1 -0
- package/dist/skills/trust.d.ts.map +1 -0
- package/dist/telemetry/logger.d.ts.map +1 -0
- package/dist/tools/permissions.d.ts.map +1 -0
- package/dist/tools/protocol.d.ts.map +1 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/sandbox/docker.d.ts.map +1 -0
- package/dist/tools/sandbox/index.d.ts.map +1 -0
- package/dist/tools/tools/ask-user.d.ts.map +1 -0
- package/dist/tools/tools/bash.d.ts.map +1 -0
- package/dist/tools/tools/edit.d.ts.map +1 -0
- package/dist/tools/tools/glob.d.ts.map +1 -0
- package/dist/tools/tools/grep.d.ts.map +1 -0
- package/dist/tools/tools/read.d.ts.map +1 -0
- package/dist/tools/tools/web-fetch.d.ts.map +1 -0
- package/dist/tools/tools/write.d.ts.map +1 -0
- package/dist/tools/validation.d.ts.map +1 -0
- package/dist/utils/markdown.d.ts.map +1 -0
- package/dist/utils/streaming.d.ts.map +1 -0
- package/dist/utils/tokens.d.ts.map +1 -0
- package/docs/SKILL_AUTHORING.md +175 -0
- package/package.json +38 -0
- package/src/agent/conversation.ts +140 -0
- package/src/agent/loop.ts +215 -0
- package/src/agent/planner.ts +55 -0
- package/src/auth/client.ts +19 -0
- package/src/auth/config.ts +89 -0
- package/src/commands/chat.ts +28 -0
- package/src/commands/config.ts +36 -0
- package/src/commands/login.ts +63 -0
- package/src/commands/marketplace.ts +190 -0
- package/src/commands/models.ts +74 -0
- package/src/compact/history-store.ts +101 -0
- package/src/compact/microcompact.ts +49 -0
- package/src/compact/service.ts +154 -0
- package/src/context/analyzer.ts +127 -0
- package/src/context/builder.ts +123 -0
- package/src/context/cache.ts +11 -0
- package/src/context/git-context.ts +58 -0
- package/src/context/llmtune-md.ts +48 -0
- package/src/context/workspace.ts +139 -0
- package/src/index.ts +100 -0
- package/src/marketplace/client.ts +118 -0
- package/src/memory/files.ts +81 -0
- package/src/memory/service.ts +124 -0
- package/src/repl/repl.ts +400 -0
- package/src/skills/args.ts +35 -0
- package/src/skills/builtin/explain-code/SKILL.md +30 -0
- package/src/skills/frontmatter.ts +47 -0
- package/src/skills/loader.ts +25 -0
- package/src/skills/registry.ts +155 -0
- package/src/skills/signing/signer.ts +101 -0
- package/src/skills/trust.ts +50 -0
- package/src/telemetry/logger.ts +108 -0
- package/src/tools/permissions.ts +83 -0
- package/src/tools/protocol.ts +24 -0
- package/src/tools/registry.ts +93 -0
- package/src/tools/sandbox/docker.ts +225 -0
- package/src/tools/sandbox/index.ts +91 -0
- package/src/tools/tools/ask-user.ts +60 -0
- package/src/tools/tools/bash.ts +97 -0
- package/src/tools/tools/edit.ts +111 -0
- package/src/tools/tools/glob.ts +68 -0
- package/src/tools/tools/grep.ts +121 -0
- package/src/tools/tools/read.ts +57 -0
- package/src/tools/tools/web-fetch.ts +158 -0
- package/src/tools/tools/write.ts +52 -0
- package/src/tools/validation.ts +164 -0
- package/src/utils/markdown.ts +96 -0
- package/src/utils/streaming.ts +63 -0
- package/src/utils/tokens.ts +41 -0
- 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
|
+
}
|