@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,155 +0,0 @@
1
- import * as fs from "fs"
2
- import * as path from "path"
3
- import * as os from "os"
4
- import { parseFrontmatter } from "./frontmatter"
5
- import { substituteArguments } from "./args"
6
-
7
- export interface Skill {
8
- name: string
9
- description: string
10
- content: string
11
- loadedFrom: "user" | "project" | "managed"
12
- userInvocable: boolean
13
- allowedTools: string[]
14
- argNames: string[]
15
- trustLevel: TrustLevel
16
- skillRoot: string
17
- }
18
-
19
- export type TrustLevel = "local" | "community" | "verified" | "signed"
20
-
21
- export interface SkillExecution {
22
- systemPrompt: string
23
- userMessage: string
24
- allowedTools?: string[]
25
- }
26
-
27
- const SKILL_DIRS = () => {
28
- const dirs: string[] = []
29
- const primary = path.join(os.homedir(), ".llmtune", "skills")
30
- dirs.push(primary)
31
- return dirs
32
- }
33
-
34
- export class SkillRegistry {
35
- private skills: Map<string, Skill> = new Map()
36
-
37
- loadAll(projectRoot?: string): void {
38
- this.skills.clear()
39
-
40
- for (const dir of SKILL_DIRS()) {
41
- this.loadFromDir(dir, "user")
42
- }
43
-
44
- const managedEnv = process.env.LLMTUNE_MANAGED_SKILLS_DIR
45
- if (managedEnv) {
46
- this.loadFromDir(managedEnv, "managed")
47
- }
48
-
49
- if (projectRoot) {
50
- const projectDir = path.join(projectRoot, ".llmtune", "skills")
51
- this.loadFromDir(projectDir, "project")
52
- const compatDir = path.join(projectRoot, ".claude", "skills")
53
- this.loadFromDir(compatDir, "project")
54
- }
55
- }
56
-
57
- private loadFromDir(baseDir: string, loadedFrom: Skill["loadedFrom"]): void {
58
- const resolved = path.resolve(baseDir)
59
- if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) return
60
-
61
- for (const entry of fs.readdirSync(resolved).sort()) {
62
- const skillPath = path.join(resolved, entry)
63
- if (!fs.statSync(skillPath).isDirectory()) continue
64
-
65
- const mdPath = path.join(skillPath, "SKILL.md")
66
- if (!fs.existsSync(mdPath)) continue
67
-
68
- const raw = fs.readFileSync(mdPath, "utf-8")
69
- const { frontmatter, body } = parseFrontmatter(raw)
70
-
71
- const description = String(frontmatter.description ?? extractFirstLine(body) ?? `Skill: ${entry}`)
72
- const userInvocable = frontmatter["user-invocable"] !== false
73
- const allowedTools = asStrList(frontmatter["allowed-tools"])
74
- const argNames = parseArgNames(frontmatter.arguments)
75
- const trustLevel = resolveTrustLevel(frontmatter.trust, loadedFrom)
76
-
77
- const skill: Skill = {
78
- name: entry,
79
- description,
80
- content: body,
81
- loadedFrom,
82
- userInvocable,
83
- allowedTools,
84
- argNames,
85
- trustLevel,
86
- skillRoot: skillPath,
87
- }
88
-
89
- if (!this.skills.has(entry.toLowerCase())) {
90
- this.skills.set(entry.toLowerCase(), skill)
91
- }
92
- }
93
- }
94
-
95
- get(name: string): Skill | undefined {
96
- return this.skills.get(name.toLowerCase())
97
- }
98
-
99
- list(): Skill[] {
100
- return Array.from(this.skills.values())
101
- }
102
-
103
- listUserInvocable(): Skill[] {
104
- return this.list().filter((s) => s.userInvocable)
105
- }
106
-
107
- prepareExecution(name: string, args: string[]): SkillExecution | null {
108
- const skill = this.get(name)
109
- if (!skill) return null
110
-
111
- const argMap: Record<string, string> = {}
112
- for (let i = 0; i < Math.min(args.length, skill.argNames.length); i++) {
113
- argMap[skill.argNames[i]] = args[i]
114
- }
115
-
116
- const userMessage = substituteArguments(skill.content, argMap)
117
-
118
- return {
119
- systemPrompt: `You are executing the "${skill.name}" skill. ${skill.description}`,
120
- userMessage,
121
- allowedTools: skill.allowedTools.length > 0 ? skill.allowedTools : undefined,
122
- }
123
- }
124
- }
125
-
126
- function resolveTrustLevel(raw: unknown, loadedFrom: Skill["loadedFrom"]): TrustLevel {
127
- const str = String(raw ?? "").toLowerCase().trim()
128
- if (str === "signed") return "signed"
129
- if (str === "verified") return "verified"
130
- if (str === "community") return "community"
131
- if (loadedFrom === "user" || loadedFrom === "project") return "local"
132
- return "community"
133
- }
134
-
135
- function extractFirstLine(body: string): string | null {
136
- for (const line of body.split("\n")) {
137
- const stripped = line.trim()
138
- if (!stripped || stripped.startsWith("#")) continue
139
- return stripped.slice(0, 200)
140
- }
141
- return null
142
- }
143
-
144
- function parseArgNames(raw: unknown): string[] {
145
- return asStrList(raw)
146
- }
147
-
148
- function asStrList(val: unknown): string[] {
149
- if (val == null) return []
150
- if (Array.isArray(val)) return val.map(String).filter(Boolean)
151
- const s = String(val).trim()
152
- if (!s) return []
153
- if (s.includes(",")) return s.split(",").map((x) => x.trim()).filter(Boolean)
154
- return [s]
155
- }
@@ -1,101 +0,0 @@
1
- import * as fs from "fs"
2
- import * as path from "path"
3
- import * as crypto from "crypto"
4
-
5
- export interface SkillSignature {
6
- algorithm: string
7
- signature: string
8
- publicKey: string
9
- signedAt: string
10
- skillName: string
11
- contentHash: string
12
- }
13
-
14
- const SIGNATURE_FILE = "SIGNATURE.json"
15
-
16
- export function hashSkillContent(skillDir: string): string {
17
- const mdPath = path.join(skillDir, "SKILL.md")
18
- if (!fs.existsSync(mdPath)) {
19
- throw new Error(`No SKILL.md found in ${skillDir}`)
20
- }
21
- const content = fs.readFileSync(mdPath, "utf-8")
22
- return crypto.createHash("sha256").update(content).digest("hex")
23
- }
24
-
25
- export function signSkill(
26
- skillDir: string,
27
- privateKey: string,
28
- publicKey: string,
29
- ): SkillSignature {
30
- const contentHash = hashSkillContent(skillDir)
31
- const skillName = path.basename(skillDir)
32
-
33
- const signature = crypto
34
- .createSign("sha256")
35
- .update(contentHash)
36
- .sign(privateKey, "base64")
37
-
38
- const sig: SkillSignature = {
39
- algorithm: "sha256-rsa",
40
- signature,
41
- publicKey,
42
- signedAt: new Date().toISOString(),
43
- skillName,
44
- contentHash,
45
- }
46
-
47
- const sigPath = path.join(skillDir, SIGNATURE_FILE)
48
- fs.writeFileSync(sigPath, JSON.stringify(sig, null, 2), "utf-8")
49
-
50
- return sig
51
- }
52
-
53
- export function verifySkill(
54
- skillDir: string,
55
- expectedPublicKey?: string,
56
- ): { valid: boolean; reason?: string } {
57
- const sigPath = path.join(skillDir, SIGNATURE_FILE)
58
- const mdPath = path.join(skillDir, "SKILL.md")
59
-
60
- if (!fs.existsSync(sigPath)) {
61
- return { valid: false, reason: "No signature file found" }
62
- }
63
- if (!fs.existsSync(mdPath)) {
64
- return { valid: false, reason: "No SKILL.md found" }
65
- }
66
-
67
- try {
68
- const sig: SkillSignature = JSON.parse(fs.readFileSync(sigPath, "utf-8"))
69
- const content = fs.readFileSync(mdPath, "utf-8")
70
- const contentHash = crypto.createHash("sha256").update(content).digest("hex")
71
-
72
- if (contentHash !== sig.contentHash) {
73
- return { valid: false, reason: "Content hash mismatch - skill has been modified" }
74
- }
75
-
76
- if (expectedPublicKey && sig.publicKey !== expectedPublicKey) {
77
- return { valid: false, reason: "Public key mismatch - signed by different author" }
78
- }
79
-
80
- const verifier = crypto.createVerify("sha256")
81
- verifier.update(sig.contentHash)
82
- const isValid = verifier.verify(sig.publicKey, sig.signature, "base64")
83
-
84
- if (!isValid) {
85
- return { valid: false, reason: "Signature verification failed" }
86
- }
87
-
88
- return { valid: true }
89
- } catch (err) {
90
- return { valid: false, reason: `Verification error: ${(err as Error).message}` }
91
- }
92
- }
93
-
94
- export function generateKeyPair(): { publicKey: string; privateKey: string } {
95
- const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
96
- modulusLength: 2048,
97
- publicKeyEncoding: { type: "spki", format: "pem" },
98
- privateKeyEncoding: { type: "pkcs8", format: "pem" },
99
- })
100
- return { publicKey, privateKey }
101
- }
@@ -1,50 +0,0 @@
1
- import type { Skill } from "./registry"
2
-
3
- export type TrustLevel = "local" | "community" | "verified" | "signed"
4
-
5
- export interface TrustPolicy {
6
- allowedTools: string[]
7
- canExecuteBash: boolean
8
- canWriteFiles: boolean
9
- }
10
-
11
- const POLICIES: Record<TrustLevel, TrustPolicy> = {
12
- local: {
13
- allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
14
- canExecuteBash: true,
15
- canWriteFiles: true,
16
- },
17
- community: {
18
- allowedTools: ["read", "glob", "grep"],
19
- canExecuteBash: false,
20
- canWriteFiles: false,
21
- },
22
- verified: {
23
- allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
24
- canExecuteBash: true,
25
- canWriteFiles: true,
26
- },
27
- signed: {
28
- allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
29
- canExecuteBash: true,
30
- canWriteFiles: true,
31
- },
32
- }
33
-
34
- export function getTrustPolicy(level: TrustLevel): TrustPolicy {
35
- return POLICIES[level]
36
- }
37
-
38
- export function resolveTrustLevel(skill: Skill): TrustLevel {
39
- if (skill.loadedFrom === "project" || skill.loadedFrom === "user") return "local"
40
- if (skill.trustLevel) return skill.trustLevel
41
- return "community"
42
- }
43
-
44
- export function isToolAllowedForSkill(
45
- toolName: string,
46
- trustLevel: TrustLevel
47
- ): boolean {
48
- const policy = POLICIES[trustLevel]
49
- return policy.allowedTools.includes(toolName.toLowerCase())
50
- }
@@ -1,108 +0,0 @@
1
- import * as fs from "fs"
2
- import * as path from "path"
3
- import * as os from "os"
4
- import { getConfigDir } from "../auth/config"
5
-
6
- export type TelemetryEvent =
7
- | { event: "tool_call"; tool: string; latency_ms: number; input_preview?: string }
8
- | { event: "llm_response"; tokens_in: number; tokens_out: number; cost: number; model: string; latency_ms: number }
9
- | { event: "compaction"; tokens_saved: number; messages_before: number; messages_after: number; trigger: string }
10
- | { event: "error"; source: string; error: string; tool?: string }
11
- | { event: "session_start"; model: string; tools: string[]; cwd: string }
12
- | { event: "session_end"; duration_ms: number; total_tool_calls: number; total_tokens: number }
13
-
14
- interface SessionLog {
15
- sessionId: string
16
- startedAt: string
17
- events: TelemetryEvent[]
18
- }
19
-
20
- let currentSession: SessionLog | null = null
21
- let sessionStartTime = 0
22
-
23
- function getLogsDir(): string {
24
- const base = process.env.LLMTUNE_LOGS_DIR || path.join(getConfigDir(), "logs")
25
- if (!fs.existsSync(base)) {
26
- fs.mkdirSync(base, { recursive: true })
27
- }
28
- return base
29
- }
30
-
31
- function getSessionLogPath(sessionId: string): string {
32
- return path.join(getLogsDir(), `${sessionId}.jsonl`)
33
- }
34
-
35
- export function startSessionLog(sessionId: string, meta: { model: string; tools: string[]; cwd: string }): void {
36
- currentSession = {
37
- sessionId,
38
- startedAt: new Date().toISOString(),
39
- events: [],
40
- }
41
- sessionStartTime = Date.now()
42
- logEvent({ event: "session_start", model: meta.model, tools: meta.tools, cwd: meta.cwd })
43
- }
44
-
45
- export function logEvent(event: TelemetryEvent): void {
46
- if (!currentSession) return
47
- currentSession.events.push(event)
48
-
49
- const entry = {
50
- ts: new Date().toISOString(),
51
- session_id: currentSession.sessionId,
52
- ...event,
53
- }
54
-
55
- try {
56
- const logPath = getSessionLogPath(currentSession.sessionId)
57
- fs.appendFileSync(logPath, JSON.stringify(entry) + "\n", "utf-8")
58
- } catch {
59
- // Telemetry write failure is non-critical
60
- }
61
- }
62
-
63
- export function endSessionLog(stats: { totalToolCalls: number; totalTokens: number }): void {
64
- if (!currentSession) return
65
- const durationMs = Date.now() - sessionStartTime
66
- logEvent({
67
- event: "session_end",
68
- duration_ms: durationMs,
69
- total_tool_calls: stats.totalToolCalls,
70
- total_tokens: stats.totalTokens,
71
- })
72
- currentSession = null
73
- }
74
-
75
- export function getSessionLogs(sessionId: string): TelemetryEvent[] {
76
- const logPath = getSessionLogPath(sessionId)
77
- try {
78
- if (!fs.existsSync(logPath)) return []
79
- const lines = fs.readFileSync(logPath, "utf-8").split("\n").filter(Boolean)
80
- return lines.map((line) => {
81
- try { return JSON.parse(line) as TelemetryEvent }
82
- catch { return null }
83
- }).filter((e): e is TelemetryEvent => e !== null)
84
- } catch {
85
- return []
86
- }
87
- }
88
-
89
- export function listSessionLogs(): Array<{ sessionId: string; size: number; modified: string }> {
90
- const dir = getLogsDir()
91
- try {
92
- if (!fs.existsSync(dir)) return []
93
- return fs.readdirSync(dir)
94
- .filter((f) => f.endsWith(".jsonl"))
95
- .map((f) => {
96
- const fullPath = path.join(dir, f)
97
- const stat = fs.statSync(fullPath)
98
- return {
99
- sessionId: f.replace(".jsonl", ""),
100
- size: stat.size,
101
- modified: stat.mtime.toISOString(),
102
- }
103
- })
104
- .sort((a, b) => b.modified.localeCompare(a.modified))
105
- } catch {
106
- return []
107
- }
108
- }
@@ -1,83 +0,0 @@
1
- import inquirer from "@inquirer/prompts"
2
- import chalk from "chalk"
3
-
4
- export type PermissionBehavior = "allow" | "deny" | "ask"
5
-
6
- export interface PermissionCheckResult {
7
- behavior: PermissionBehavior
8
- message?: string
9
- suggestion?: string
10
- updatedInput?: Record<string, unknown>
11
- }
12
-
13
- export interface PermissionConfig {
14
- allowedTools: Set<string>
15
- deniedTools: Set<string>
16
- sessionTrust: Map<string, boolean>
17
- }
18
-
19
- export class PermissionManager {
20
- private config: PermissionConfig
21
-
22
- constructor() {
23
- this.config = {
24
- allowedTools: new Set(),
25
- deniedTools: new Set(),
26
- sessionTrust: new Map(),
27
- }
28
- }
29
-
30
- trustTool(toolName: string): void {
31
- this.config.sessionTrust.set(toolName, true)
32
- }
33
-
34
- isTrusted(toolName: string): boolean {
35
- return this.config.sessionTrust.get(toolName) === true
36
- }
37
-
38
- async check(
39
- toolName: string,
40
- input: Record<string, unknown>,
41
- isDestructive: boolean
42
- ): Promise<PermissionCheckResult> {
43
- if (this.isTrusted(toolName)) {
44
- return { behavior: "allow" }
45
- }
46
-
47
- if (this.config.deniedTools.has(toolName)) {
48
- return { behavior: "deny", message: `Tool '${toolName}' is denied` }
49
- }
50
-
51
- if (isDestructive) {
52
- const command = toolName === "bash" ? (input.command as string) : ""
53
- const preview = command
54
- ? command.slice(0, 80) + (command.length > 80 ? "..." : "")
55
- : JSON.stringify(input).slice(0, 100)
56
-
57
- console.log(chalk.yellow(`\n⚠ ${toolName} wants to execute:`))
58
- console.log(chalk.dim(preview))
59
-
60
- const confirmed = await inquirer.confirm({
61
- message: `Allow ${toolName}? (y/N)`,
62
- default: false,
63
- })
64
-
65
- if (!confirmed) {
66
- return { behavior: "deny", message: "User denied" }
67
- }
68
-
69
- const alwaysTrust = await inquirer.confirm({
70
- message: `Trust ${toolName} for this session? (y/N)`,
71
- default: false,
72
- })
73
-
74
- if (alwaysTrust) {
75
- this.trustTool(toolName)
76
- }
77
-
78
- return { behavior: "allow" }
79
- }
80
-
81
- return { behavior: "allow" }
82
- }
83
- }
@@ -1,24 +0,0 @@
1
- export interface ToolSpec {
2
- name: string
3
- description: string
4
- inputSchema: Record<string, unknown>
5
- isReadOnly?: boolean
6
- isDestructive?: boolean
7
- }
8
-
9
- export interface ToolResult {
10
- name: string
11
- output: unknown
12
- isError: boolean
13
- toolUseId?: string
14
- }
15
-
16
- export interface ToolContext {
17
- cwd: string
18
- workspaceRoot: string
19
- }
20
-
21
- export interface Tool {
22
- spec(): ToolSpec
23
- run(input: Record<string, unknown>, ctx: ToolContext): ToolResult | Promise<ToolResult>
24
- }
@@ -1,93 +0,0 @@
1
- import type { Tool, ToolSpec, ToolResult, ToolContext } from "./protocol"
2
- import { validateJsonSchema } from "./validation"
3
-
4
- export type { Tool, ToolSpec, ToolResult, ToolContext }
5
-
6
- export interface ToolDefinition {
7
- name: string
8
- description: string
9
- inputSchema: Record<string, unknown>
10
- isReadOnly?: boolean
11
- isDestructive?: boolean
12
- run: (input: Record<string, unknown>, ctx: ToolContext) => ToolResult | Promise<ToolResult>
13
- }
14
-
15
- class ToolInstance implements Tool {
16
- private def: ToolDefinition
17
- constructor(def: ToolDefinition) { this.def = def }
18
- spec(): ToolSpec {
19
- return {
20
- name: this.def.name,
21
- description: this.def.description,
22
- inputSchema: this.def.inputSchema,
23
- isReadOnly: this.def.isReadOnly,
24
- isDestructive: this.def.isDestructive,
25
- }
26
- }
27
- run(input: Record<string, unknown>, ctx: ToolContext): ToolResult | Promise<ToolResult> {
28
- return this.def.run(input, ctx)
29
- }
30
- }
31
-
32
- export function createTool(def: ToolDefinition): Tool {
33
- return new ToolInstance(def)
34
- }
35
-
36
- export class ToolRegistry {
37
- private tools: Map<string, Tool> = new Map()
38
-
39
- register(tool: Tool): void {
40
- const spec = tool.spec()
41
- const key = spec.name.toLowerCase()
42
- if (this.tools.has(key)) {
43
- throw new Error(`duplicate tool name: ${spec.name}`)
44
- }
45
- this.tools.set(key, tool)
46
- }
47
-
48
- listSpecs(): ToolSpec[] {
49
- return Array.from(this.tools.values()).map((t) => t.spec())
50
- }
51
-
52
- get(name: string): Tool | undefined {
53
- return this.tools.get(name.toLowerCase())
54
- }
55
-
56
- dispatch(name: string, input: Record<string, unknown>, ctx: ToolContext): ToolResult {
57
- const tool = this.get(name)
58
- if (!tool) {
59
- return { name, output: { error: `unknown tool: ${name}` }, isError: true }
60
- }
61
-
62
- const spec = tool.spec()
63
- try {
64
- validateJsonSchema(input, spec.inputSchema)
65
- } catch (err: unknown) {
66
- const msg = err instanceof Error ? err.message : String(err)
67
- return { name: spec.name, output: { error: msg }, isError: true }
68
- }
69
-
70
- const result = tool.run(input, ctx)
71
- if (result instanceof Promise) {
72
- throw new Error("Async tools not supported in sync dispatch. Use dispatchAsync instead.")
73
- }
74
- return result
75
- }
76
-
77
- async dispatchAsync(name: string, input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {
78
- const tool = this.get(name)
79
- if (!tool) {
80
- return { name, output: { error: `unknown tool: ${name}` }, isError: true }
81
- }
82
-
83
- const spec = tool.spec()
84
- try {
85
- validateJsonSchema(input, spec.inputSchema)
86
- } catch (err: unknown) {
87
- const msg = err instanceof Error ? err.message : String(err)
88
- return { name: spec.name, output: { error: msg }, isError: true }
89
- }
90
-
91
- return tool.run(input, ctx)
92
- }
93
- }