@desk2quant/cli 1.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.
Files changed (4) hide show
  1. package/README.md +66 -0
  2. package/d2q.mjs +137 -0
  3. package/engine.mjs +298 -0
  4. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Desk2Quant Quant Agent CLI
2
+
3
+ A terminal-based quant learning, problem-solving, interview and project assistant backed by Desk2Quant.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 20+
8
+ - A paid Desk2Quant purchase using the email you sign in with
9
+
10
+ ## Install
11
+
12
+ Once the package is published:
13
+
14
+ ```bash
15
+ npm install -g @desk2quant/cli
16
+ ```
17
+
18
+ For development from this repository:
19
+
20
+ ```bash
21
+ cd cli
22
+ npm link
23
+ ```
24
+
25
+ ## Sign in
26
+
27
+ ```bash
28
+ d2q login you@example.com
29
+ ```
30
+
31
+ Desk2Quant emails the existing My Access magic link. Paste the complete link into the terminal. The CLI exchanges it for a signed Quant Agent session. Model, Razorpay, Supabase and email-service secrets always remain server-side.
32
+
33
+ ## Main commands
34
+
35
+ ```bash
36
+ d2q learn "Ito's lemma"
37
+ d2q solve "derive E[S_T^2] under GBM"
38
+ d2q practice "conditional probability"
39
+ d2q interview "quant research"
40
+ d2q project "Heston calibration"
41
+ d2q progress
42
+ ```
43
+
44
+ ## Adaptive assessment
45
+
46
+ ```bash
47
+ d2q assess probability
48
+ d2q submit <assessment-id> "your answer"
49
+ d2q skills
50
+ ```
51
+
52
+ `theta` is a bounded latent ability estimate updated from graded assessments. It is not a percentage or percentile.
53
+
54
+ ## Private Desk2Quant retrieval
55
+
56
+ When the signed-in buyer has a Razorpay-verified entitlement to an indexed Desk2Quant product, relevant excerpts from that product can be retrieved server-side and used as grounding. The CLI cannot query the private corpus directly and no paid-book text is bundled with this package.
57
+
58
+ ## Local credentials
59
+
60
+ The CLI stores only the signed Desk2Quant agent session in `~/.desk2quant/config.json`, created with owner-only permissions where supported. Run:
61
+
62
+ ```bash
63
+ d2q logout
64
+ ```
65
+
66
+ to remove it.
package/d2q.mjs ADDED
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ import readline from 'node:readline/promises';
3
+ import { stdin as input, stdout as output } from 'node:process';
4
+ import {
5
+ COMMANDS,
6
+ HELP,
7
+ exchangeMagicLink,
8
+ formatProgress,
9
+ formatSkills,
10
+ formatTerminalMath,
11
+ getProgress,
12
+ getSkills,
13
+ loadConfig,
14
+ logout,
15
+ requestLogin,
16
+ runCommand,
17
+ startAssessment,
18
+ submitAssessment
19
+ } from './engine.mjs';
20
+
21
+ const VERSION = '1.1.0';
22
+
23
+ function hasFlag(args, flag) { return args.includes(flag); }
24
+ function withoutFlags(args) { return args.filter(a => !a.startsWith('--')); }
25
+ function print(value, json = false) {
26
+ if (json) return console.log(JSON.stringify(value, null, 2));
27
+ if (typeof value === 'string') return console.log(formatTerminalMath(value));
28
+ if (value?.content) {
29
+ console.log(formatTerminalMath(value.content));
30
+ if (value.meta?.remainingToday !== undefined) console.log(`\n[${value.meta.remainingToday} requests remaining today]`);
31
+ return;
32
+ }
33
+ console.log(value);
34
+ }
35
+
36
+ async function login(email, rl, json = false) {
37
+ await requestLogin(email);
38
+ if (!json) {
39
+ console.log('A Desk2Quant sign-in link has been requested.');
40
+ console.log('Open your email, copy the complete "My Access" sign-in URL, and paste it below.');
41
+ }
42
+ const link = (await rl.question('Magic link > ')).trim();
43
+ const result = await exchangeMagicLink(link);
44
+ if (json) return print({ success: true, email: result.config.email, tier: result.tier, expiresAt: result.expiresAt, progress: result.progress }, true);
45
+ console.log(`Signed in as ${result.config.email} (${result.tier}).`);
46
+ if (result.progress) console.log('\n' + formatProgress(result.progress));
47
+ }
48
+
49
+ async function execute(command, rest, rl, json = false) {
50
+ if (command === 'help') return console.log(HELP);
51
+ if (command === 'login') {
52
+ const email = rest.join(' ').trim();
53
+ if (!email) throw new Error('Usage: d2q login <purchase-email>');
54
+ return login(email, rl, json);
55
+ }
56
+ if (command === 'logout') {
57
+ const removed = await logout();
58
+ return print(json ? { success: true, removed } : (removed ? 'Signed out.' : 'No local Desk2Quant session was present.'), json);
59
+ }
60
+ if (command === 'whoami') {
61
+ const cfg = await loadConfig();
62
+ if (!cfg) throw new Error('Not signed in. Run `d2q login <purchase-email>`.');
63
+ const info = { email: cfg.email, tier: cfg.tier, expiresAt: cfg.expiresAt, baseUrl: cfg.baseUrl };
64
+ return print(json ? info : `${cfg.email} (${cfg.tier})\nSession expires: ${new Date(Number(cfg.expiresAt)).toLocaleString()}`, json);
65
+ }
66
+ if (command === 'progress') {
67
+ const result = await getProgress();
68
+ return print(json ? result : formatProgress(result.progress), json);
69
+ }
70
+ if (command === 'skills') {
71
+ const result = await getSkills();
72
+ return print(json ? result : `${formatSkills(result.skills)}\n\n${result.note || ''}`.trim(), json);
73
+ }
74
+ if (command === 'assess') {
75
+ const result = await startAssessment(rest.join(' ').trim());
76
+ if (json) return print(result, true);
77
+ console.log(`Assessment ID: ${result.assessmentId}`);
78
+ console.log(`Skill: ${result.skill} | difficulty b=${Number(result.difficulty).toFixed(2)} | current theta=${Number(result.currentTheta).toFixed(2)}`);
79
+ console.log(`\n${formatTerminalMath(result.question)}`);
80
+ console.log(`\nSubmit with:\n d2q submit ${result.assessmentId} "your answer"`);
81
+ return;
82
+ }
83
+ if (command === 'submit') {
84
+ const assessmentId = rest.shift();
85
+ const answer = rest.join(' ').trim();
86
+ const result = await submitAssessment(assessmentId, answer);
87
+ if (json) return print(result, true);
88
+ console.log(`Score: ${(Number(result.score) * 100).toFixed(1)}%`);
89
+ console.log(`Theta: ${Number(result.thetaBefore).toFixed(2)} -> ${Number(result.thetaAfter).toFixed(2)}`);
90
+ console.log(`Attempts: ${result.attempts}`);
91
+ console.log(`\n${formatTerminalMath(result.feedback)}`);
92
+ console.log(`\n${formatTerminalMath(result.abilityNote)}`);
93
+ return;
94
+ }
95
+ if (COMMANDS.has(command)) {
96
+ const query = rest.join(' ').trim();
97
+ const result = await runCommand(command, query);
98
+ return print(result, json);
99
+ }
100
+ throw new Error(`Unknown command "${command}". Run d2q --help.`);
101
+ }
102
+
103
+ async function main() {
104
+ const rawArgs = process.argv.slice(2);
105
+ if (rawArgs.includes('--help') || rawArgs.includes('-h')) return console.log(HELP);
106
+ if (rawArgs.includes('--version') || rawArgs.includes('-v')) return console.log(`Desk2Quant Quant Agent CLI ${VERSION}`);
107
+ const json = hasFlag(rawArgs, '--json');
108
+ const args = withoutFlags(rawArgs);
109
+ const rl = readline.createInterface({ input, output });
110
+
111
+ try {
112
+ if (args.length) return await execute(args[0].toLowerCase(), args.slice(1), rl, json);
113
+
114
+ console.log(`Desk2Quant Quant Agent CLI ${VERSION}`);
115
+ const cfg = await loadConfig();
116
+ console.log(cfg ? `Signed in: ${cfg.email}` : 'Not signed in. Type: login <purchase-email>');
117
+ console.log('Type help for commands, or exit.');
118
+
119
+ while (true) {
120
+ const line = (await rl.question('d2q > ')).trim();
121
+ if (!line) continue;
122
+ if (['exit', 'quit'].includes(line.toLowerCase())) break;
123
+ const parts = line.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
124
+ const clean = parts.map(p => p.replace(/^"|"$/g, ''));
125
+ const command = String(clean.shift() || '').toLowerCase();
126
+ try { await execute(command, clean, rl, false); }
127
+ catch (err) { console.error(`Error: ${err.message}`); }
128
+ }
129
+ } finally {
130
+ rl.close();
131
+ }
132
+ }
133
+
134
+ main().catch((err) => {
135
+ console.error(`Desk2Quant CLI: ${err.message}`);
136
+ process.exitCode = 1;
137
+ });
package/engine.mjs ADDED
@@ -0,0 +1,298 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ export const COMMANDS = new Set(['learn','solve','practice','interview','project']);
6
+ export const ASSESSMENT_SKILLS = ['probability','linear_algebra','statistics','stochastic_calculus','derivatives','fixed_income','numerical_methods','programming','risk','quant_research'];
7
+ export const BASE_URL = process.env.D2Q_BASE_URL || 'https://desk2quant.com';
8
+ export const HELP = `Desk2Quant Quant Agent CLI
9
+
10
+ Authentication:
11
+ d2q login <purchase-email> Email a Desk2Quant magic link, then paste it
12
+ d2q logout Remove the local agent session
13
+ d2q whoami Show the signed-in account and tier
14
+ d2q progress Show activity and today's usage
15
+
16
+ Quant Agent:
17
+ d2q learn <topic>
18
+ d2q solve <problem>
19
+ d2q practice <topic>
20
+ d2q interview <role/topic>
21
+ d2q project <topic>
22
+
23
+ Adaptive Assessment:
24
+ d2q assess <skill> Start one calibrated assessment question
25
+ d2q submit <id> <answer> Grade the answer and update latent skill theta
26
+ d2q skills Show calibrated skill estimates
27
+
28
+ Skills:
29
+ ${ASSESSMENT_SKILLS.join(', ')}
30
+
31
+ Options:
32
+ --json Print machine-readable JSON
33
+ --help, -h Show help
34
+ --version, -v Show version
35
+
36
+ Security:
37
+ The CLI never stores or receives Desk2Quant's model/API secrets.
38
+ Its local session file is created with owner-only permissions.`;
39
+
40
+ function configDir(env = process.env) {
41
+ return env.D2Q_CONFIG_DIR || path.join(os.homedir(), '.desk2quant');
42
+ }
43
+
44
+ export function configPath(env = process.env) {
45
+ return path.join(configDir(env), 'config.json');
46
+ }
47
+
48
+ export function normalizeCommand(command='') {
49
+ const c = String(command).trim().toLowerCase();
50
+ if (COMMANDS.has(c)) return c;
51
+ throw new Error(`Unknown command "${command}". Use: ${[...COMMANDS].join(', ')}`);
52
+ }
53
+
54
+ export function parseMagicLink(value='') {
55
+ const raw = String(value).trim();
56
+ if (!raw) throw new Error('Magic link is required.');
57
+ let url;
58
+ try { url = new URL(raw); }
59
+ catch { throw new Error('Paste the complete Desk2Quant sign-in link from your email.'); }
60
+ const email = String(url.searchParams.get('email') || '').trim().toLowerCase();
61
+ const token = String(url.searchParams.get('tk') || '').trim();
62
+ if (!email || !email.includes('@') || !token) {
63
+ throw new Error('That link does not contain a valid Desk2Quant email/token pair.');
64
+ }
65
+ const host = url.hostname.toLowerCase();
66
+ const allowed = host === 'desk2quant.com' || host.endsWith('.vercel.app') || host === 'localhost' || host === '127.0.0.1';
67
+ if (!allowed) throw new Error('Refusing a sign-in link from an untrusted host.');
68
+ return { email, accessToken: token };
69
+ }
70
+
71
+ function terminalMathSegment(value='') {
72
+ let text = String(value);
73
+
74
+ // Strip display/inline LaTeX wrappers and layout-only commands that terminals
75
+ // cannot typeset. Keep the mathematical content itself.
76
+ text = text
77
+ .replace(/\\\[/g, '\n')
78
+ .replace(/\\\]/g, '\n')
79
+ .replace(/\\\(/g, '')
80
+ .replace(/\\\)/g, '')
81
+ .replace(/\\begin\{(?:aligned|align\*?|equation\*?|gather\*?)\}/g, '')
82
+ .replace(/\\end\{(?:aligned|align\*?|equation\*?|gather\*?)\}/g, '')
83
+ .replace(/\\(?:left|right|bigl|bigr|Bigl|Bigr|big|Big)\b/g, '')
84
+ .replace(/\\!/g, '')
85
+ .replace(/\\[,;:]/g, ' ')
86
+ .replace(/\\qquad\b/g, ' ')
87
+ .replace(/\\quad\b/g, ' ')
88
+ .replace(/\\\\/g, '\n');
89
+
90
+ // Fractions are intentionally rendered as explicit parenthesized division.
91
+ // Repeat to handle simple nested fractions without adding a parser dependency.
92
+ for (let i = 0; i < 8; i += 1) {
93
+ const next = text.replace(/\\(?:d|t)?frac\s*\{([^{}]+)\}\s*\{([^{}]+)\}/g, '($1)/($2)');
94
+ if (next === text) break;
95
+ text = next;
96
+ }
97
+
98
+ text = text
99
+ .replace(/\\sqrt\s*\{([^{}]+)\}/g, '√($1)')
100
+ .replace(/\\mathbb\{Q\}/g, 'ℚ')
101
+ .replace(/\\mathbb\{R\}/g, 'ℝ')
102
+ .replace(/\\mathbb\{P\}/g, 'ℙ')
103
+ .replace(/\\text\{([^{}]*)\}/g, '$1')
104
+ .replace(/\\operatorname\{([^{}]*)\}/g, '$1')
105
+ .replace(/\\boxed\{([^{}]*)\}/g, '$1');
106
+
107
+ const symbols = {
108
+ theta: 'θ', sigma: 'σ', mu: 'μ', alpha: 'α', beta: 'β', gamma: 'γ',
109
+ delta: 'δ', rho: 'ρ', lambda: 'λ', kappa: 'κ', phi: 'φ',
110
+ Delta: 'Δ', Gamma: 'Γ', Theta: 'Θ', Lambda: 'Λ',
111
+ partial: '∂', nabla: '∇', int: '∫', sum: 'Σ', prod: 'Π', infinity: '∞', infty: '∞',
112
+ approx: '≈', neq: '≠', leq: '≤', geq: '≥', le: '≤', ge: '≥',
113
+ times: '×', cdot: '·', pm: '±', to: '→', rightarrow: '→', leftarrow: '←'
114
+ };
115
+ for (const [command, symbol] of Object.entries(symbols)) {
116
+ text = text.replace(new RegExp(`\\\\${command}\\b`, 'g'), symbol);
117
+ }
118
+
119
+ text = text
120
+ .replace(/\\exp\b/g, 'exp')
121
+ .replace(/\\ln\b/g, 'ln')
122
+ .replace(/\\log\b/g, 'log')
123
+ .replace(/\\sin\b/g, 'sin')
124
+ .replace(/\\cos\b/g, 'cos')
125
+ .replace(/\\min\b/g, 'min')
126
+ .replace(/\\max\b/g, 'max')
127
+ .replace(/_\{([^{}]+)\}/g, '_$1')
128
+ .replace(/\^\{([^{}]+)\}/g, '^($1)')
129
+ .replace(/\^2\b/g, '²')
130
+ .replace(/\^3\b/g, '³')
131
+ .replace(/\^\(-1\)/g, '⁻¹')
132
+ .replace(/(?<!\\)\$([^$\n]+)\$/g, '$1')
133
+ .replace(/[ \t]+\n/g, '\n')
134
+ .replace(/\n{3,}/g, '\n\n');
135
+
136
+ return text;
137
+ }
138
+
139
+ export function formatTerminalMath(value='') {
140
+ const text = String(value ?? '').replace(/\r\n/g, '\n');
141
+ if (!text) return text;
142
+
143
+ // Never rewrite code examples. Only human-readable prose/math segments are
144
+ // normalized, so Python/R/SQL snippets remain byte-for-byte intact.
145
+ return text
146
+ .split(/(```[\s\S]*?```|`[^`\n]+`)/g)
147
+ .map(part => part.startsWith('`') ? part : terminalMathSegment(part))
148
+ .join('')
149
+ .trim();
150
+ }
151
+
152
+ async function readJson(response) {
153
+ const text = await response.text();
154
+ let data;
155
+ try { data = text ? JSON.parse(text) : {}; }
156
+ catch { throw new Error(`Desk2Quant returned an invalid response (${response.status}).`); }
157
+ if (!response.ok) {
158
+ const err = new Error(data.error || `Desk2Quant request failed (${response.status}).`);
159
+ err.status = response.status;
160
+ err.data = data;
161
+ throw err;
162
+ }
163
+ return data;
164
+ }
165
+
166
+ export async function apiPost(route, payload, { baseUrl = BASE_URL, fetchImpl = fetch } = {}) {
167
+ const response = await fetchImpl(`${String(baseUrl).replace(/\/$/, '')}${route}`, {
168
+ method: 'POST',
169
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'Desk2Quant-CLI/1.1' },
170
+ body: JSON.stringify(payload)
171
+ });
172
+ return readJson(response);
173
+ }
174
+
175
+ export async function requestLogin(email, options = {}) {
176
+ const normalized = String(email || '').trim().toLowerCase();
177
+ if (!normalized || !normalized.includes('@')) throw new Error('A valid purchase email is required.');
178
+ return apiPost('/api/interview', { action: 'access-login', email: normalized }, options);
179
+ }
180
+
181
+ export async function exchangeMagicLink(link, options = {}) {
182
+ const parsed = parseMagicLink(link);
183
+ const data = await apiPost('/api/products', {
184
+ action: 'agent-auth',
185
+ email: parsed.email,
186
+ accessToken: parsed.accessToken
187
+ }, options);
188
+ const config = {
189
+ version: 1,
190
+ email: parsed.email,
191
+ tier: data.tier || 'pro',
192
+ agentToken: data.agentToken,
193
+ expiresAt: data.expiresAt,
194
+ baseUrl: options.baseUrl || BASE_URL
195
+ };
196
+ if (!config.agentToken) throw new Error('Desk2Quant did not return an agent session.');
197
+ await saveConfig(config, options.env || process.env);
198
+ return { ...data, config };
199
+ }
200
+
201
+ export async function saveConfig(config, env = process.env) {
202
+ const dir = configDir(env);
203
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
204
+ const file = configPath(env);
205
+ const tmp = `${file}.${process.pid}.tmp`;
206
+ await fs.writeFile(tmp, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
207
+ await fs.rename(tmp, file);
208
+ try { await fs.chmod(file, 0o600); } catch (_) {}
209
+ }
210
+
211
+ export async function loadConfig(env = process.env) {
212
+ try {
213
+ const raw = await fs.readFile(configPath(env), 'utf8');
214
+ const cfg = JSON.parse(raw);
215
+ if (!cfg?.email || !cfg?.agentToken) return null;
216
+ return cfg;
217
+ } catch (err) {
218
+ if (err?.code === 'ENOENT') return null;
219
+ throw new Error('Could not read ~/.desk2quant/config.json. Run `d2q logout` and sign in again.');
220
+ }
221
+ }
222
+
223
+ export async function logout(env = process.env) {
224
+ try { await fs.unlink(configPath(env)); return true; }
225
+ catch (err) { if (err?.code === 'ENOENT') return false; throw err; }
226
+ }
227
+
228
+ function assertSession(config) {
229
+ if (!config) throw new Error('Not signed in. Run `d2q login <purchase-email>`.');
230
+ if (config.expiresAt && Date.now() >= Number(config.expiresAt)) {
231
+ throw new Error('Your Desk2Quant agent session expired. Run `d2q login <purchase-email>` again.');
232
+ }
233
+ }
234
+
235
+ async function sessionPost(action, extra = {}, options = {}) {
236
+ const config = options.config || await loadConfig(options.env || process.env);
237
+ assertSession(config);
238
+ return apiPost('/api/products', {
239
+ action,
240
+ email: config.email,
241
+ agentToken: config.agentToken,
242
+ ...extra
243
+ }, {
244
+ baseUrl: options.baseUrl || config.baseUrl || BASE_URL,
245
+ fetchImpl: options.fetchImpl || fetch
246
+ });
247
+ }
248
+
249
+ export async function runCommand(command, query='', options = {}) {
250
+ const c = normalizeCommand(command);
251
+ const q = String(query).trim();
252
+ if (!q) throw new Error(`${c} requires a topic/problem.`);
253
+ return sessionPost('agent-run', { command: c, query: q }, options);
254
+ }
255
+
256
+ export async function getProgress(options = {}) {
257
+ return sessionPost('agent-progress', {}, options);
258
+ }
259
+
260
+ export async function startAssessment(skill, options = {}) {
261
+ const s = String(skill || '').trim();
262
+ if (!s) throw new Error('Usage: d2q assess <skill>');
263
+ return sessionPost('agent-assess-start', { skill: s }, options);
264
+ }
265
+
266
+ export async function submitAssessment(assessmentId, answer, options = {}) {
267
+ const id = String(assessmentId || '').trim();
268
+ const response = String(answer || '').trim();
269
+ if (!id || !response) throw new Error('Usage: d2q submit <assessment-id> <answer>');
270
+ return sessionPost('agent-assess-submit', { assessmentId: id, answer: response }, options);
271
+ }
272
+
273
+ export async function getSkills(options = {}) {
274
+ return sessionPost('agent-skills', {}, options);
275
+ }
276
+
277
+ export function formatProgress(progress = {}) {
278
+ const lines = [
279
+ `Total sessions: ${Number(progress.totalSessions) || 0}`,
280
+ `Today: ${Number(progress.usedToday) || 0}/${Number(progress.dailyLimit) || 0} used (${Number(progress.remainingToday) || 0} remaining)`
281
+ ];
282
+ if (progress.lastCommand) lines.push(`Last: ${progress.lastCommand} — ${progress.lastTopic || 'general quant'}`);
283
+ if (Array.isArray(progress.topTopics) && progress.topTopics.length) {
284
+ lines.push('Top topics:');
285
+ for (const item of progress.topTopics) lines.push(` ${item.topic}: ${item.sessions}`);
286
+ }
287
+ if (progress.note) lines.push(`\n${progress.note}`);
288
+ return lines.join('\n');
289
+ }
290
+
291
+ export function formatSkills(skills = []) {
292
+ if (!Array.isArray(skills) || !skills.length) return 'No graded skill assessments yet. Run `d2q assess <skill>`.';
293
+ return skills.map(s => {
294
+ const theta = Number(s.theta) || 0;
295
+ const score = Math.round((Number(s.mean_score) || 0) * 100);
296
+ return `${s.skill_key}: theta=${theta.toFixed(2)} | attempts=${Number(s.attempts)||0} | mean score=${score}%`;
297
+ }).join('\n');
298
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@desk2quant/cli",
3
+ "version": "1.1.0",
4
+ "description": "Desk2Quant Quant Agent CLI for aspiring quants",
5
+ "type": "module",
6
+ "bin": {
7
+ "d2q": "d2q.mjs"
8
+ },
9
+ "files": [
10
+ "d2q.mjs",
11
+ "engine.mjs",
12
+ "README.md"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/AIM-IT4/desk2quant.git",
17
+ "directory": "cli"
18
+ },
19
+ "homepage": "https://desk2quant.com",
20
+ "bugs": {
21
+ "url": "https://github.com/AIM-IT4/desk2quant/issues"
22
+ },
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "license": "UNLICENSED",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "keywords": [
31
+ "quant",
32
+ "quantitative-finance",
33
+ "interview",
34
+ "desk2quant",
35
+ "cli"
36
+ ]
37
+ }