@jpetit/toolkit 3.0.15 → 3.0.17

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/lib/ai.ts ADDED
@@ -0,0 +1,144 @@
1
+ import { encode } from 'gpt-tokenizer'
2
+ import { estimateCost } from 'gpt-tokenizer/model/gpt-5'
3
+ import { igniteModel, LlmModel, loadModels, logger, Message } from 'multi-llm-ts'
4
+
5
+ // do not log anything from multi-llm-ts
6
+ logger.disable()
7
+
8
+ export async function complete(model: string, systemPrompt: string, userPrompt: string): Promise<string> {
9
+ const parts = model.split('/')
10
+ const providerName = parts[0]!
11
+ const modelName = parts[1]!
12
+
13
+ const config = { apiKey: process.env[keys[providerName]!] || '' }
14
+
15
+ const models = await loadModels(providerName, config)
16
+ // console.log(models)
17
+ const chat = models!.chat.find((m) => m.id === modelName)!
18
+
19
+ const bot = igniteModel(providerName, chat, config)
20
+ const messages = [new Message('system', systemPrompt), new Message('user', userPrompt)]
21
+ const response = await bot.complete(messages)
22
+ return response.content!
23
+ }
24
+
25
+ type ModelInfo = Record<string, Record<string, string[]>>
26
+
27
+ export async function listModels(): Promise<ModelInfo> {
28
+ const result: ModelInfo = {}
29
+ const providers = Object.keys(keys).sort()
30
+ for (const providerName of providers) {
31
+ if (providerName === 'ollama') continue // Ollama models are local, skip for now
32
+ const config = { apiKey: process.env[keys[providerName]!] || '' }
33
+ const models = await loadModels(providerName, config)
34
+ if (models === null) continue
35
+ result[providerName] = {}
36
+ for (const modelType in models) {
37
+ result[providerName][modelType] = []
38
+ for (const model of models.chat) {
39
+ result[providerName][modelType].push(model.id)
40
+ }
41
+ }
42
+ }
43
+ return result
44
+ }
45
+
46
+ export class ChatBot {
47
+ private model: string
48
+ private bot: LlmModel | null = null
49
+ private messages: Message[]
50
+ public totalInputTokens: number = 0
51
+ public totalOutputTokens: number = 0
52
+ public totalInputCost: number = 0
53
+ public totalOutputCost: number = 0
54
+
55
+ constructor(model: string, systemPrompt: string) {
56
+ this.model = model
57
+ this.messages = [new Message('system', systemPrompt)]
58
+ }
59
+
60
+ async init() {
61
+ const parts = this.model.split('/')
62
+ const providerName = parts[0]!
63
+ const modelName = parts[1]!
64
+ const config = { apiKey: process.env[keys[providerName]!] || '' }
65
+ const models = await loadModels(providerName, config)
66
+ const chat = models!.chat.find((m: any) => m.id === modelName)!
67
+ this.bot = igniteModel(providerName, chat, config)
68
+ }
69
+
70
+ async complete(userPrompt: string): Promise<string> {
71
+ if (!this.bot) {
72
+ throw new Error(`Model '${this.model}' could not be initialized`)
73
+ }
74
+
75
+ this.messages.push(new Message('user', userPrompt))
76
+ const response = await this.bot.complete(this.messages)
77
+ this.messages.push(new Message('assistant', response.content))
78
+
79
+ const inputTokens = encode(userPrompt).length
80
+ const outputTokens = encode(response.content!).length
81
+ const inputCost = estimateCost(inputTokens)
82
+ const outputCost = estimateCost(outputTokens)
83
+
84
+ this.totalInputTokens += inputTokens
85
+ this.totalOutputTokens += outputTokens
86
+ this.totalInputCost += inputCost.main!.input!
87
+ this.totalOutputCost += outputCost.main!.output!
88
+
89
+ return response.content!
90
+ }
91
+
92
+ forgetLastInteraction() {
93
+ if (this.messages.length > 2) {
94
+ this.messages.pop()
95
+ this.messages.pop()
96
+ }
97
+ }
98
+
99
+ modelInformation(): any {
100
+ if (!this.bot) {
101
+ throw new Error(`Model '${this.model}' has not been initialized`)
102
+ }
103
+ return this.bot.model
104
+ }
105
+ }
106
+
107
+ export type PowerEstimation = {
108
+ wattHours: number
109
+ joules: number
110
+ co2Grams: number
111
+ trees: number
112
+ }
113
+
114
+ export function estimatePowerConsumption(inputTokens: number, outputTokens: number): PowerEstimation {
115
+ // Written by Claude.ai
116
+
117
+ // Very rough estimates based on public data
118
+ // One tree absorbs approximately:
119
+ // - 22 kg (22,000g) of CO2 per year on average
120
+ // - 1 ton (1,000,000g) over its lifetime (~40 years)
121
+
122
+ const wattsPerToken = 0.0003 // ~0.3 milliwatts per token
123
+ const totalTokens = inputTokens + outputTokens
124
+ const co2PerTree = 22000
125
+
126
+ return {
127
+ wattHours: (totalTokens * wattsPerToken) / 3600, // Convert to Wh
128
+ joules: totalTokens * wattsPerToken,
129
+ co2Grams: ((totalTokens * wattsPerToken) / 3600) * 0.5, // Rough CO2 estimate
130
+ trees: (((totalTokens * wattsPerToken) / 3600) * 0.5) / co2PerTree,
131
+ }
132
+ }
133
+
134
+ const keys: Record<string, string> = {
135
+ google: 'GEMINI_API_KEY',
136
+ openai: 'OPENAI_API_KEY',
137
+ ollama: '',
138
+ }
139
+
140
+ export function cleanMardownCodeString(s: string): string {
141
+ const pattern = /^\n*```\w*\s*(.*?)\s*```\n*$/s
142
+ const clean = s.replace(pattern, '$1')
143
+ return clean
144
+ }
package/lib/cleaner.ts ADDED
@@ -0,0 +1,66 @@
1
+ import tui from './tui'
2
+ import { confirm } from '@inquirer/prompts'
3
+ import { readdir, rm } from 'fs/promises'
4
+ import { join } from 'path'
5
+
6
+ export async function cleanFiles(force: boolean, directory: string): Promise<void> {
7
+ const patterns = [
8
+ '\\.exe$',
9
+ '\\.cor$',
10
+ '\\.out$',
11
+ '\\.pyc$',
12
+ '\\.class$',
13
+ '\\.o$',
14
+ '\\.ho$',
15
+ '~$',
16
+ '^problem\\.[a-z][a-z]\\.ps$',
17
+ '^problem\\.[a-z][a-z]\\.md$',
18
+ '^problem\\.[a-z][a-z]\\.pdf$',
19
+ '^problem\\.[a-z][a-z]\\.txt$',
20
+ '^problem\\.[a-z][a-z]\\.html$',
21
+ '^a\\.out$',
22
+ '^__pycache__$',
23
+ ]
24
+ const pattern = new RegExp(patterns.join('|'))
25
+
26
+ const entries = await readdir(directory, { withFileTypes: true })
27
+
28
+ const removalList: string[] = []
29
+ for (const entry of entries) {
30
+ const fullPath = join(directory, entry.name)
31
+ if (pattern.test(entry.name)) {
32
+ removalList.push(fullPath)
33
+ }
34
+ }
35
+
36
+ if (removalList.length === 0) {
37
+ tui.success('No files/directories to remove')
38
+ return
39
+ }
40
+
41
+ tui.warning(`The following ${removalList.length} files/directories will be removed:`)
42
+ for (const elem of removalList.sort()) {
43
+ tui.print(elem)
44
+ }
45
+
46
+ if (!force) {
47
+ console.log()
48
+ const conformation = await confirm({
49
+ message: `Remove ${removalList.length} files/directories?`,
50
+ default: false,
51
+ })
52
+ if (!conformation) return
53
+ }
54
+
55
+ let removalCount = 0
56
+ for (const elem of removalList) {
57
+ try {
58
+ await rm(elem, { recursive: true, force: true })
59
+ removalCount++
60
+ } catch (error) {
61
+ tui.error(`Could not remove file/directory ${elem}`)
62
+ }
63
+ }
64
+
65
+ tui.success(`Removed ${removalCount} files/directories`)
66
+ }
@@ -0,0 +1,97 @@
1
+ import tui from '..//tui'
2
+ import { execa } from 'execa'
3
+ import { exists, rm } from 'fs/promises'
4
+ import { join, sep } from 'path'
5
+ import { readText } from '..//utils'
6
+ import type { Handler } from '..//types'
7
+
8
+ export type CompilerInfo = {
9
+ compiler_id: string
10
+ name: string
11
+ language: string
12
+ version: string
13
+ flags1: string
14
+ flags2: string
15
+ extension: string
16
+ type: string
17
+ warning: string
18
+ }
19
+
20
+ export abstract class Compiler {
21
+ abstract id(): string
22
+
23
+ abstract name(): string
24
+
25
+ abstract type(): string
26
+
27
+ abstract language(): string
28
+
29
+ abstract version(): Promise<string>
30
+
31
+ abstract flags1(): string
32
+
33
+ abstract flags2(): string
34
+
35
+ abstract extension(): string
36
+
37
+ warning(): string {
38
+ return ''
39
+ }
40
+
41
+ async available(): Promise<boolean> {
42
+ const version = await this.version()
43
+ return version !== 'not found'
44
+ }
45
+
46
+ async info(): Promise<CompilerInfo> {
47
+ return {
48
+ compiler_id: this.id(),
49
+ name: this.name(),
50
+ language: this.language(),
51
+ version: await this.version(),
52
+ flags1: this.flags1(),
53
+ flags2: this.flags2(),
54
+ extension: this.extension(),
55
+ type: this.type(),
56
+ warning: this.warning(),
57
+ }
58
+ }
59
+
60
+ abstract compile(handler: Handler, directory: string, sourcePath: string): Promise<void>
61
+
62
+ // Default implementation of execute for compiled languages
63
+ async execute(handler: Handler, directory: string, inputPath: string, outputPath: string): Promise<void> {
64
+ const executablePath = `solution.${this.extension()}.exe`
65
+ if (!(await exists(join(directory, executablePath)))) {
66
+ throw new Error(`Executable file ${executablePath} does not exist in directory ${directory}`)
67
+ }
68
+ // TODO: check in windows
69
+ const relativeExecutablePath = `.${sep}${executablePath}` // force prepending ./ to make it work
70
+ const fullInputPath = join(directory, inputPath)
71
+ const fullOutputPath = join(directory, outputPath)
72
+ await rm(fullOutputPath, { force: true })
73
+ const input = await readText(fullInputPath)
74
+
75
+ tui.command(`${relativeExecutablePath} < ${inputPath} > ${outputPath}`)
76
+
77
+ const { exitCode } = await execa({
78
+ reject: false,
79
+ input,
80
+ stdout: { file: fullOutputPath },
81
+ stderr: 'inherit',
82
+ cwd: directory,
83
+ })`${relativeExecutablePath}`
84
+
85
+ if (exitCode !== 0) throw new Error(`Execution failed for ${executablePath} with exit code ${exitCode}`)
86
+ }
87
+
88
+ protected async getVersion(cmd: string, lineIndex: number): Promise<string> {
89
+ try {
90
+ const { stdout } = await execa`${cmd.split(' ')}`
91
+ const lines = stdout.split('\n')
92
+ return lines[lineIndex]?.trim() || 'Unknown version'
93
+ } catch {
94
+ return 'not found'
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,44 @@
1
+ // TODO: all!
2
+
3
+ import { type Handler } from '..//types'
4
+ import { nothing } from '..//utils'
5
+ import { Compiler } from './base'
6
+
7
+ export class Clojure_Compiler extends Compiler {
8
+ id(): string {
9
+ return 'Clojure'
10
+ }
11
+
12
+ name(): string {
13
+ return 'Clojure'
14
+ }
15
+
16
+ type(): string {
17
+ return 'vm'
18
+ }
19
+
20
+ language(): string {
21
+ return 'Clojure'
22
+ }
23
+
24
+ async version(): Promise<string> {
25
+ return await this.getVersion('clj --version', 0)
26
+ }
27
+
28
+ flags1(): string {
29
+ return ''
30
+ }
31
+
32
+ flags2(): string {
33
+ return ''
34
+ }
35
+
36
+ extension(): string {
37
+ return 'clj'
38
+ }
39
+
40
+ async compile(handler: Handler, directory: string, sourcePath: string): Promise<void> {
41
+ await nothing()
42
+ throw new Error(`Not implemented`)
43
+ }
44
+ }
@@ -0,0 +1,68 @@
1
+ import { type Handler } from '..//types'
2
+ import tui from '..//tui'
3
+ import { Compiler } from './base'
4
+ import { rm, exists } from 'fs/promises'
5
+ import { join } from 'path'
6
+ import { execa } from 'execa'
7
+
8
+ export class GCC_Compiler extends Compiler {
9
+ id(): string {
10
+ return 'GCC'
11
+ }
12
+
13
+ name(): string {
14
+ return 'GNU C Compiler'
15
+ }
16
+
17
+ type(): string {
18
+ return 'compiler'
19
+ }
20
+
21
+ language(): string {
22
+ return 'C'
23
+ }
24
+
25
+ async version(): Promise<string> {
26
+ return await this.getVersion('gcc --version', 0)
27
+ }
28
+
29
+ flags1(): string {
30
+ return '-D_JUDGE_ -O2 -DNDEBUG -Wall -Wextra -Wno-sign-compare'
31
+ }
32
+
33
+ flags2(): string {
34
+ return ''
35
+ }
36
+
37
+ extension(): string {
38
+ return 'c'
39
+ }
40
+
41
+ async compile(handler: Handler, directory: string, sourcePath: string): Promise<void> {
42
+ const exePath = sourcePath + '.exe'
43
+ const fullExePath = join(directory, exePath)
44
+ await rm(fullExePath, { force: true })
45
+
46
+ if (handler.source_modifier === 'none') {
47
+ tui.command(`gcc ${this.flags1()} ${sourcePath} -o ${exePath}`)
48
+ await execa({
49
+ reject: false,
50
+ stderr: 'inherit',
51
+ stdout: 'inherit',
52
+ cwd: directory,
53
+ })`gcc ${this.flags1().split(' ')} ${sourcePath} -o ${exePath}`
54
+ } else if (handler.source_modifier === 'no_main') {
55
+ tui.command(`gcc ${this.flags1()} ${sourcePath} main.cc -o ${exePath}`)
56
+ await execa({
57
+ reject: false,
58
+ stderr: 'inherit',
59
+ stdout: 'inherit',
60
+ cwd: directory,
61
+ })`gcc ${this.flags1().split(' ')} ${sourcePath} main.cc -o ${exePath}`
62
+ } else {
63
+ throw new Error(`Unknown source modifier: ${handler.source_modifier}`)
64
+ }
65
+
66
+ if (!(await exists(fullExePath))) throw new Error(`Compilation failed for ${sourcePath}`)
67
+ }
68
+ }
@@ -0,0 +1,60 @@
1
+ import { type Handler } from '..//types'
2
+ import tui from '..//tui'
3
+ import { Compiler } from './base'
4
+ import { rm, exists } from 'fs/promises'
5
+ import { join } from 'path'
6
+ import { execa } from 'execa'
7
+
8
+ export class GHC_Compiler extends Compiler {
9
+ id(): string {
10
+ return 'GHC'
11
+ }
12
+
13
+ name(): string {
14
+ return 'GHC'
15
+ }
16
+
17
+ type(): string {
18
+ return 'compiler'
19
+ }
20
+
21
+ language(): string {
22
+ return 'Haskell'
23
+ }
24
+
25
+ async version(): Promise<string> {
26
+ return await this.getVersion('ghc --version', 0)
27
+ }
28
+
29
+ flags1(): string {
30
+ return '-O3'
31
+ }
32
+
33
+ flags2(): string {
34
+ return '-O3'
35
+ }
36
+
37
+ extension(): string {
38
+ return 'hs'
39
+ }
40
+
41
+ async compile(handler: Handler, directory: string, sourcePath: string): Promise<void> {
42
+ const exePath = sourcePath + '.exe'
43
+ const fullExePath = join(directory, exePath)
44
+ await rm(fullExePath, { force: true })
45
+
46
+ if (handler.source_modifier === 'none') {
47
+ tui.command(`ghc ${this.flags1()} ${sourcePath} -o ${exePath}`)
48
+ await execa({
49
+ reject: false,
50
+ stderr: 'inherit',
51
+ stdout: 'inherit',
52
+ cwd: directory,
53
+ })`ghc ${this.flags1().split(' ')} ${sourcePath} -o ${exePath}`
54
+ } else {
55
+ throw new Error(`Unknown source modifier: ${handler.source_modifier}`)
56
+ }
57
+
58
+ if (!(await exists(fullExePath))) throw new Error(`Compilation failed for ${sourcePath}`)
59
+ }
60
+ }
@@ -0,0 +1,68 @@
1
+ import { execa } from 'execa'
2
+ import { exists, rm } from 'fs/promises'
3
+ import { join } from 'path'
4
+ import tui from '../tui'
5
+ import { type Handler } from '../types'
6
+ import { Compiler } from './base'
7
+
8
+ export class GXX_Compiler extends Compiler {
9
+ id(): string {
10
+ return 'GXX'
11
+ }
12
+
13
+ name(): string {
14
+ return 'GNU C++ Compiler'
15
+ }
16
+
17
+ type(): string {
18
+ return 'compiler'
19
+ }
20
+
21
+ language(): string {
22
+ return 'C++'
23
+ }
24
+
25
+ async version(): Promise<string> {
26
+ return await this.getVersion('g++ --version', 0)
27
+ }
28
+
29
+ flags1(): string {
30
+ return '-std=c++17 -D_JUDGE_ -O2 -DNDEBUG -Wall -Wextra -Wno-sign-compare -Wshadow'
31
+ }
32
+
33
+ flags2(): string {
34
+ return '-std=c++17 -D_JUDGE_ -O2 -DNDEBUG -Wall -Wextra -Wno-sign-compare -Wshadow'
35
+ }
36
+
37
+ extension(): string {
38
+ return 'cc'
39
+ }
40
+
41
+ async compile(handler: Handler, directory: string, sourcePath: string): Promise<void> {
42
+ const outputPath = sourcePath + '.exe'
43
+ const fullOutputPath = join(directory, outputPath)
44
+ await rm(fullOutputPath, { force: true })
45
+
46
+ if (handler.source_modifier === 'none') {
47
+ tui.command(`g++ ${this.flags1()} ${sourcePath} -o ${outputPath}`)
48
+ await execa({
49
+ reject: false,
50
+ stderr: 'inherit',
51
+ stdout: 'inherit',
52
+ cwd: directory,
53
+ })`g++ ${this.flags1().split(' ')} ${sourcePath} -o ${outputPath}`
54
+ } else if (handler.source_modifier === 'no_main') {
55
+ tui.command(`g++ ${this.flags1()} ${sourcePath} main.cc -o ${outputPath}`)
56
+ await execa({
57
+ reject: false,
58
+ stderr: 'inherit',
59
+ stdout: 'inherit',
60
+ cwd: directory,
61
+ })`g++ ${this.flags1().split(' ')} ${sourcePath} main.cc -o ${outputPath}`
62
+ } else {
63
+ throw new Error(`Unknown source modifier: ${handler.source_modifier}`)
64
+ }
65
+
66
+ if (!(await exists(fullOutputPath))) throw new Error(`Compilation failed for ${sourcePath}`)
67
+ }
68
+ }
@@ -0,0 +1,72 @@
1
+ import { nothing } from '../utils'
2
+ import { Compiler, type CompilerInfo } from './base'
3
+ import { GCC_Compiler } from './gcc'
4
+ import { GXX_Compiler } from './gxx'
5
+ import { Python3_Compiler } from './python3'
6
+ import { GHC_Compiler } from './ghc'
7
+ import { Clojure_Compiler } from './clojure'
8
+ import { RunPython_Compiler } from './run-python'
9
+ import { RunHaskell_Compiler } from './run-haskell'
10
+
11
+ const compilersRegistryById: Record<string, new () => Compiler> = {
12
+ C: GCC_Compiler,
13
+ 'C++': GXX_Compiler,
14
+ Python3: Python3_Compiler,
15
+ Haskell: GHC_Compiler,
16
+ Clojure: Clojure_Compiler,
17
+ RunPython: RunPython_Compiler,
18
+ RunHaskell: RunHaskell_Compiler,
19
+ }
20
+
21
+ const compilersRegistryByExtension: Record<string, new () => Compiler> = {
22
+ c: GCC_Compiler,
23
+ cc: GXX_Compiler,
24
+ py: Python3_Compiler,
25
+ hs: GHC_Compiler,
26
+ clj: Clojure_Compiler,
27
+ }
28
+
29
+ export function getCompilerById(id: string): Compiler {
30
+ const CompilerClass = compilersRegistryById[id]
31
+ if (!CompilerClass) throw new Error(`Compiler '${id}' is not defined`)
32
+ const compilerInstance = new CompilerClass()
33
+ return compilerInstance
34
+ }
35
+
36
+ export function getCompilerByExtension(extension: string): Compiler {
37
+ const CompilerClass = compilersRegistryByExtension[extension]
38
+ if (!CompilerClass) throw new Error(`No compiler defined for extension '.${extension}'`)
39
+ const compilerInstance = new CompilerClass()
40
+ return compilerInstance
41
+ }
42
+
43
+ export async function getDefinedCompilerIds(): Promise<string[]> {
44
+ await nothing()
45
+ return Object.keys(compilersRegistryById)
46
+ }
47
+
48
+ export async function getAvailableCompilers(): Promise<string[]> {
49
+ const available: string[] = []
50
+ for (const id of Object.keys(compilersRegistryById)) {
51
+ const CompilerClass = compilersRegistryById[id]!
52
+ const compilerInstance = new CompilerClass()
53
+ if (await compilerInstance.available()) {
54
+ available.push(id)
55
+ }
56
+ }
57
+ return available
58
+ }
59
+
60
+ export async function getCompilerInfo(id: string): Promise<CompilerInfo> {
61
+ const compilerInstance = getCompilerById(id)
62
+ if (!compilerInstance) throw new Error(`Compiler '${id}' is not defined`)
63
+ return await compilerInstance.info()
64
+ }
65
+
66
+ export async function getCompilersInfo(): Promise<Record<string, CompilerInfo>> {
67
+ const infos: Record<string, CompilerInfo> = {}
68
+ for (const id of Object.keys(compilersRegistryById)) {
69
+ infos[id] = await getCompilerInfo(id)
70
+ }
71
+ return infos
72
+ }