feat(qmd): add shared Node helpers and index.yml model block editing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Julian lechnerandClaude Opus 5.5 committed 2026-10-08 11:15:08 +02:00
1 parent c7c7c57621
commit 1ad1880704
4 files changed
+201

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
// shared/qmd/qmd-config.mjs — edits only the models: block of QMD's index.yml.
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { MODELS, qmdConfigPath, qmdPackageDir } from './qmd-lib.mjs';
export function setModels(text, YAML) {
const document = YAML.parseDocument(text || '');
if (document.errors.length) throw new Error(`index.yml cannot be parsed: ${document.errors[0].message}`);
if (document.contents === null) document.contents = document.createNode({});
document.set('models', document.createNode({ ...MODELS }));
return document.toString();
}
export function unsetModels(text, YAML) {
const document = YAML.parseDocument(text || '');
if (document.errors.length) throw new Error(`index.yml cannot be parsed: ${document.errors[0].message}`);
if (document.contents !== null) document.delete('models');
return document.contents === null || document.contents.items?.length === 0 ? '' : document.toString();
}
async function main(action) {
if (!['set-models', 'unset-models'].includes(action)) throw new Error('usage: qmd-config.mjs set-models|unset-models');
const YAML = await import(pathToFileURL(join(qmdPackageDir(), 'node_modules', 'yaml', 'dist', 'index.js')).href);
const path = qmdConfigPath();
const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
const next = action === 'set-models' ? setModels(current, YAML) : unsetModels(current, YAML);
mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, next);
}
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
main(process.argv[2]).catch((error) => { console.error(`qmd-config: ${error.message}`); process.exit(1); });
}
+83
View File
@@ -0,0 +1,83 @@
// shared/qmd/qmd-lib.mjs
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
export const MODELS = {
embed: 'hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf',
rerank: 'hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf',
generate: 'hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf',
};
export const MASK = '**/*.{md,py,cs,ts,tsx,js,jsx,mjs,go,rs,java,ps1,sh,json,yml,yaml,xml,sql}';
export function collectionName(repoPath) {
return 'repo-' + createHash('sha256').update(resolve(repoPath), 'utf8').digest('hex').slice(0, 12);
}
export const stateDir = (home = homedir()) => join(home, '.my-ai-configuration', 'qmd');
export const setupStatePath = (home = homedir()) => join(home, '.my-ai-configuration', 'qmd.json');
export function qmdConfigPath(env = process.env, home = homedir()) {
const dir = env.QMD_CONFIG_DIR || (env.XDG_CONFIG_HOME ? join(env.XDG_CONFIG_HOME, 'qmd') : join(home, '.config', 'qmd'));
return join(dir, 'index.yml');
}
export function qmdDbPath(env = process.env, home = homedir()) {
return join(env.XDG_CACHE_HOME || join(home, '.cache'), 'qmd', 'index.sqlite');
}
export function deviceEnv(device, env = process.env) {
const result = { ...env };
if (device === 'cpu') {
result.QMD_FORCE_CPU = '1';
delete result.QMD_LLAMA_GPU;
} else {
result.QMD_LLAMA_GPU = 'auto';
delete result.QMD_FORCE_CPU;
}
return result;
}
export function idleMinutes(env = process.env) {
const value = Number(env.QMD_IDLE_MINUTES);
return Number.isFinite(value) && value > 0 ? value : 30;
}
export function readJson(path, fallback) {
try {
return JSON.parse(readFileSync(path, 'utf8'));
} catch {
return fallback;
}
}
// The state directory lives in the user profile; mode 0600 restricts the token on POSIX.
export function writePrivateJson(path, value) {
mkdirSync(dirname(path), { recursive: true });
const temporary = `${path}.${process.pid}.tmp`;
writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
renameSync(temporary, path);
}
let cachedPackageDir;
export function qmdPackageDir() {
if (process.env.QMD_PACKAGE_DIR) return process.env.QMD_PACKAGE_DIR;
if (!cachedPackageDir) {
const root = execFileSync('npm', ['root', '--global'], { encoding: 'utf8', shell: process.platform === 'win32', windowsHide: true }).trim();
cachedPackageDir = join(root, '@tobilu', 'qmd');
}
return cachedPackageDir;
}
export function importQmd(subpath = join('dist', 'index.js')) {
return import(pathToFileURL(join(qmdPackageDir(), subpath)).href);
}
// Runs the QMD CLI through node without a shell, so free-text arguments are never re-parsed.
export function runQmdCli(args, options = {}) {
return execFileSync(process.execPath, [join(qmdPackageDir(), 'dist', 'cli', 'qmd.js'), ...args], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, ...options });
}