escribano 0.1.4 → 0.2.2
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/dist/actions/generate-artifact-v3.js +4 -2
- package/dist/actions/generate-summary-v3.js +4 -2
- package/dist/adapters/audio.silero.adapter.js +50 -3
- package/dist/adapters/intelligence.ollama.adapter.js +9 -7
- package/dist/adapters/video.ffmpeg.adapter.js +9 -5
- package/dist/index.js +10 -0
- package/dist/services/subject-grouping.js +4 -2
- package/dist/tests/utils/env-logger.test.js +262 -0
- package/dist/utils/env-logger.js +166 -0
- package/package.json +6 -3
- package/scripts/backfill-releases.mjs +207 -0
- package/scripts/create-release.mjs +201 -0
- package/src/scripts/audio_preprocessor.py +109 -0
- package/src/scripts/visual_observer_base.py +417 -0
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
import { execSync } from 'node:child_process';
|
|
8
8
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
|
-
import path from 'node:path';
|
|
10
|
+
import path, { dirname, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
11
12
|
import { log, step } from '../pipeline/context.js';
|
|
12
13
|
import { normalizeAppNames } from '../services/app-normalization.js';
|
|
13
14
|
import { groupTopicBlocksIntoSubjects, saveSubjectsToDatabase, } from '../services/subject-grouping.js';
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
16
|
export async function generateArtifactV3(recordingId, repos, intelligence, options) {
|
|
15
17
|
const format = options.format || 'card';
|
|
16
18
|
log('info', `[Artifact V3.1] Generating ${format} artifact for recording ${recordingId}...`);
|
|
@@ -204,7 +206,7 @@ async function generateLlmArtifact(subjects, groupingResult, format, recording,
|
|
|
204
206
|
: format === 'standup'
|
|
205
207
|
? 'standup.md'
|
|
206
208
|
: 'summary-v3.md';
|
|
207
|
-
const promptPath =
|
|
209
|
+
const promptPath = resolve(__dirname, '..', '..', 'prompts', promptFileName);
|
|
208
210
|
let promptTemplate;
|
|
209
211
|
try {
|
|
210
212
|
promptTemplate = await readFile(promptPath, 'utf-8');
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
|
-
import path from 'node:path';
|
|
8
|
+
import path, { dirname, resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
9
10
|
import { log } from '../pipeline/context.js';
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
12
|
/**
|
|
11
13
|
* Generate a work session summary artifact from processed TopicBlocks.
|
|
12
14
|
*
|
|
@@ -81,7 +83,7 @@ export async function generateSummaryV3(recordingId, repos, intelligence, option
|
|
|
81
83
|
*/
|
|
82
84
|
async function generateLlmSummary(sections, recording, intelligence) {
|
|
83
85
|
// Read prompt template
|
|
84
|
-
const promptPath =
|
|
86
|
+
const promptPath = resolve(__dirname, '..', '..', 'prompts', 'summary-v3.md');
|
|
85
87
|
let promptTemplate;
|
|
86
88
|
try {
|
|
87
89
|
promptTemplate = await readFile(promptPath, 'utf-8');
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { exec, spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
2
3
|
import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
3
4
|
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
+
import path, { dirname, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
5
7
|
import { promisify } from 'node:util';
|
|
6
8
|
const execAsync = promisify(exec);
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
10
|
export function createSileroPreprocessor() {
|
|
8
11
|
let currentProcess = null;
|
|
9
12
|
return {
|
|
@@ -19,19 +22,63 @@ export function createSileroPreprocessor() {
|
|
|
19
22
|
catch (error) {
|
|
20
23
|
throw new Error(`Failed to pre-convert audio for VAD: ${error.message}`);
|
|
21
24
|
}
|
|
22
|
-
const scriptPath =
|
|
25
|
+
const scriptPath = resolve(__dirname, '..', '..', 'src', 'scripts', 'audio_preprocessor.py');
|
|
26
|
+
if (!existsSync(scriptPath)) {
|
|
27
|
+
throw new Error(`Audio preprocessor script not found at: ${scriptPath}`);
|
|
28
|
+
}
|
|
23
29
|
const command = `uv run "${scriptPath}" --audio "${inputWavPath}" --output-dir "${tempDir}" --output-json "${manifestPath}"`;
|
|
24
30
|
try {
|
|
25
31
|
console.log(`Running Silero VAD on ${inputWavPath}...`);
|
|
32
|
+
if (process.env.ESCRIBANO_VERBOSE === 'true') {
|
|
33
|
+
console.log(` Script path: ${scriptPath}`);
|
|
34
|
+
console.log(` Script exists: ${existsSync(scriptPath)}`);
|
|
35
|
+
console.log(` Command: ${command}`);
|
|
36
|
+
console.log(` Working directory (user): ${process.cwd()}`);
|
|
37
|
+
try {
|
|
38
|
+
const { stdout: uvVersion } = await execAsync('uv --version');
|
|
39
|
+
console.log(` uv version: ${uvVersion.trim()}`);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
console.log(` uv version: NOT FOUND`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
26
45
|
currentProcess = spawn('sh', ['-c', command]);
|
|
46
|
+
let stderr = '';
|
|
47
|
+
let stdout = '';
|
|
48
|
+
if (currentProcess.stderr) {
|
|
49
|
+
currentProcess.stderr.on('data', (data) => {
|
|
50
|
+
stderr += data.toString();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (currentProcess.stdout) {
|
|
54
|
+
currentProcess.stdout.on('data', (data) => {
|
|
55
|
+
stdout += data.toString();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
27
58
|
await new Promise((resolve, reject) => {
|
|
28
59
|
currentProcess?.on('close', (code) => {
|
|
29
60
|
currentProcess = null;
|
|
30
61
|
if (code === 0) {
|
|
62
|
+
if (process.env.ESCRIBANO_VERBOSE === 'true' && stdout) {
|
|
63
|
+
console.log(` Silero VAD stdout:\n${stdout
|
|
64
|
+
.split('\n')
|
|
65
|
+
.map((l) => ' ' + l)
|
|
66
|
+
.join('\n')}`);
|
|
67
|
+
}
|
|
31
68
|
resolve();
|
|
32
69
|
}
|
|
33
70
|
else {
|
|
34
|
-
|
|
71
|
+
console.error(` Silero VAD stderr:\n${stderr
|
|
72
|
+
.split('\n')
|
|
73
|
+
.map((l) => ' ' + l)
|
|
74
|
+
.join('\n')}`);
|
|
75
|
+
if (stdout) {
|
|
76
|
+
console.error(` Silero VAD stdout:\n${stdout
|
|
77
|
+
.split('\n')
|
|
78
|
+
.map((l) => ' ' + l)
|
|
79
|
+
.join('\n')}`);
|
|
80
|
+
}
|
|
81
|
+
reject(new Error(`Silero VAD failed with code ${code}: ${stderr || stdout || 'No output captured'}`));
|
|
35
82
|
}
|
|
36
83
|
});
|
|
37
84
|
currentProcess?.on('error', (err) => {
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* Implements IntelligenceService using Ollama REST API
|
|
5
5
|
*/
|
|
6
6
|
import { readFileSync } from 'node:fs';
|
|
7
|
-
import {
|
|
7
|
+
import { dirname, resolve } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import { Agent, fetch as undiciFetch } from 'undici';
|
|
9
10
|
import { z } from 'zod';
|
|
10
11
|
import { classificationSchema, intelligenceConfigSchema, transcriptMetadataSchema, } from '../0_types.js';
|
|
11
12
|
// Debug logging controlled by environment variable
|
|
12
13
|
const DEBUG_OLLAMA = process.env.ESCRIBANO_DEBUG_OLLAMA === 'true';
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
15
|
// TODO: put in an util
|
|
14
16
|
export function debugLog(...args) {
|
|
15
17
|
if (DEBUG_OLLAMA) {
|
|
@@ -193,7 +195,7 @@ async function classifySegmentWithOllama(segment, config, transcript) {
|
|
|
193
195
|
return raw;
|
|
194
196
|
}
|
|
195
197
|
function loadClassifySegmentPrompt(segment, transcript) {
|
|
196
|
-
const promptPath =
|
|
198
|
+
const promptPath = resolve(__dirname, '..', '..', 'prompts', 'classify-segment.md');
|
|
197
199
|
let prompt = readFileSync(promptPath, 'utf-8');
|
|
198
200
|
const timeRangeStr = `[${segment.timeRange[0]}s - ${segment.timeRange[1]}s]`;
|
|
199
201
|
const ocrContext = segment.contexts.map((c) => `${c.type}: ${c.value}`).join(', ') || 'None';
|
|
@@ -208,7 +210,7 @@ function loadClassifySegmentPrompt(segment, transcript) {
|
|
|
208
210
|
return prompt;
|
|
209
211
|
}
|
|
210
212
|
function loadClassifyPrompt(transcript, visualLogs) {
|
|
211
|
-
const promptPath =
|
|
213
|
+
const promptPath = resolve(__dirname, '..', '..', 'prompts', 'classify.md');
|
|
212
214
|
let prompt = readFileSync(promptPath, 'utf-8');
|
|
213
215
|
const segmentsText = transcript.segments
|
|
214
216
|
.map((seg) => `[seg-${seg.id}] [${seg.start}s - ${seg.end}s] ${seg.text}`)
|
|
@@ -241,7 +243,7 @@ function loadClassifyPrompt(transcript, visualLogs) {
|
|
|
241
243
|
*/
|
|
242
244
|
function buildVLMSingleImagePrompt() {
|
|
243
245
|
try {
|
|
244
|
-
const promptPath =
|
|
246
|
+
const promptPath = resolve(__dirname, '..', '..', 'prompts', 'vlm-single.md');
|
|
245
247
|
return readFileSync(promptPath, 'utf-8');
|
|
246
248
|
}
|
|
247
249
|
catch {
|
|
@@ -426,7 +428,7 @@ async function extractTopicsWithOllama(observations, config) {
|
|
|
426
428
|
return [];
|
|
427
429
|
let prompt;
|
|
428
430
|
try {
|
|
429
|
-
const promptPath =
|
|
431
|
+
const promptPath = resolve(__dirname, '..', '..', 'prompts', 'topic-extract.md');
|
|
430
432
|
const template = readFileSync(promptPath, 'utf-8');
|
|
431
433
|
prompt = template.replace('{{OBSERVATIONS}}', textSamples.join('\n---\n'));
|
|
432
434
|
}
|
|
@@ -657,7 +659,7 @@ async function extractMetadata(transcript, classification, config, visualLogs) {
|
|
|
657
659
|
return raw;
|
|
658
660
|
}
|
|
659
661
|
function loadMetadataPrompt(transcript, classification, visualLogs) {
|
|
660
|
-
const promptPath =
|
|
662
|
+
const promptPath = resolve(__dirname, '..', '..', 'prompts', 'extract-metadata.md');
|
|
661
663
|
let prompt = readFileSync(promptPath, 'utf-8');
|
|
662
664
|
const classificationSummary = Object.entries(classification)
|
|
663
665
|
.filter(([_, score]) => score >= 25)
|
|
@@ -698,7 +700,7 @@ async function generateArtifact(artifactType, context, config) {
|
|
|
698
700
|
return response;
|
|
699
701
|
}
|
|
700
702
|
function loadArtifactPrompt(artifactType, context) {
|
|
701
|
-
const promptPath =
|
|
703
|
+
const promptPath = resolve(__dirname, '..', '..', 'prompts', `${artifactType}.md`);
|
|
702
704
|
let prompt = readFileSync(promptPath, 'utf-8');
|
|
703
705
|
// TODO: Implement robust transcript cleaning (Milestone 4)
|
|
704
706
|
prompt = prompt.replace('{{TRANSCRIPT_ALL}}', context.transcript.fullText);
|
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* Used for extracting screenshots and detecting scene changes.
|
|
6
6
|
*/
|
|
7
7
|
import { exec, spawn } from 'node:child_process';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
8
9
|
import { mkdir, readdir, readFile, rm } from 'node:fs/promises';
|
|
9
10
|
import os from 'node:os';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
+
import path, { dirname, resolve } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
11
13
|
import { promisify } from 'node:util';
|
|
12
14
|
import { debugLog } from './intelligence.ollama.adapter.js';
|
|
13
15
|
const execAsync = promisify(exec);
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
17
|
// Scene detection configuration (with env var overrides)
|
|
15
18
|
// Lower threshold = more sensitive = more scene changes detected
|
|
16
19
|
// Examples: 0.3 (sensitive), 0.4 (default), 0.5 (conservative)
|
|
@@ -268,15 +271,16 @@ export function createFfmpegVideoService() {
|
|
|
268
271
|
* OCR is parallelized across all available CPU cores.
|
|
269
272
|
*/
|
|
270
273
|
runVisualIndexing: async (framesDir, outputPath) => {
|
|
271
|
-
const scriptPath =
|
|
274
|
+
const scriptPath = resolve(__dirname, '..', '..', 'src', 'scripts', 'visual_observer_base.py');
|
|
275
|
+
if (!existsSync(scriptPath)) {
|
|
276
|
+
throw new Error(`Visual observer script not found at: ${scriptPath}`);
|
|
277
|
+
}
|
|
272
278
|
const frameInterval = Number(process.env.ESCRIBANO_FRAME_INTERVAL) || 2;
|
|
273
279
|
const workers = os.cpus().length;
|
|
274
|
-
// Use uv run to execute the script with its environment
|
|
275
|
-
// --workers enables parallel OCR processing
|
|
276
280
|
const command = `uv run "${scriptPath}" --frames-dir "${framesDir}" --output "${outputPath}" --frame-interval ${frameInterval} --workers ${workers}`;
|
|
277
281
|
try {
|
|
278
282
|
await execAsync(command, {
|
|
279
|
-
cwd:
|
|
283
|
+
cwd: dirname(scriptPath),
|
|
280
284
|
});
|
|
281
285
|
const content = await readFile(outputPath, 'utf-8');
|
|
282
286
|
return JSON.parse(content);
|
package/dist/index.js
CHANGED
|
@@ -7,16 +7,22 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
10
11
|
import { createCapCaptureSource } from './adapters/capture.cap.adapter.js';
|
|
11
12
|
import { createFilesystemCaptureSource } from './adapters/capture.filesystem.adapter.js';
|
|
12
13
|
import { cleanupMlxBridge, initializeSystem, processVideo, } from './batch-context.js';
|
|
13
14
|
import { getDbPath } from './db/index.js';
|
|
14
15
|
import { checkPrerequisites, hasMissingPrerequisites, printDoctorResults, } from './prerequisites.js';
|
|
16
|
+
import { logEnvironmentVariables } from './utils/env-logger.js';
|
|
15
17
|
const MODELS_DIR = path.join(homedir(), '.escribano', 'models');
|
|
16
18
|
const MODEL_FILE = 'ggml-large-v3.bin';
|
|
17
19
|
const MODEL_PATH = path.join(MODELS_DIR, MODEL_FILE);
|
|
18
20
|
function main() {
|
|
19
21
|
const args = parseArgs(process.argv.slice(2));
|
|
22
|
+
if (args.version) {
|
|
23
|
+
console.log(`escribano v${pkg.version}`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
20
26
|
if (args.help) {
|
|
21
27
|
showHelp();
|
|
22
28
|
process.exit(0);
|
|
@@ -53,6 +59,7 @@ function parseArgs(argsArray) {
|
|
|
53
59
|
return {
|
|
54
60
|
force: argsArray.includes('--force'),
|
|
55
61
|
help: argsArray.includes('--help') || argsArray.includes('-h'),
|
|
62
|
+
version: argsArray.includes('--version') || argsArray.includes('-v'),
|
|
56
63
|
doctor: argsArray[0] === 'doctor',
|
|
57
64
|
file: filePath,
|
|
58
65
|
skipSummary: argsArray.includes('--skip-summary'),
|
|
@@ -82,6 +89,7 @@ Usage:
|
|
|
82
89
|
npx escribano --include-personal Include personal time in artifact
|
|
83
90
|
npx escribano --copy Copy artifact to clipboard
|
|
84
91
|
npx escribano --stdout Print artifact to stdout
|
|
92
|
+
npx escribano --version Show version number
|
|
85
93
|
npx escribano --help Show this help
|
|
86
94
|
|
|
87
95
|
Examples:
|
|
@@ -96,6 +104,8 @@ Output: Markdown summary saved to ~/.escribano/artifacts/
|
|
|
96
104
|
}
|
|
97
105
|
async function run(args) {
|
|
98
106
|
const { force, file: filePath, skipSummary, micAudio, systemAudio, format, includePersonal, copyToClipboard, printToStdout, } = args;
|
|
107
|
+
// Log environment variables if verbose mode is enabled
|
|
108
|
+
logEnvironmentVariables();
|
|
99
109
|
// Initialize system (reuses batch-context for consistency)
|
|
100
110
|
console.log('Initializing database...');
|
|
101
111
|
const ctx = await initializeSystem();
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* This is the foundation for the new artifact architecture.
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync } from 'node:fs';
|
|
8
|
-
import {
|
|
8
|
+
import { dirname, resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
11
|
const PERSONAL_APPS = new Set([
|
|
10
12
|
'WhatsApp',
|
|
11
13
|
'Instagram',
|
|
@@ -144,7 +146,7 @@ ID: ${b.id}`;
|
|
|
144
146
|
: `"${blockIdList[0]}"`;
|
|
145
147
|
let template;
|
|
146
148
|
try {
|
|
147
|
-
const promptPath =
|
|
149
|
+
const promptPath = resolve(__dirname, '..', '..', 'prompts', 'subject-grouping.md');
|
|
148
150
|
template = readFileSync(promptPath, 'utf-8');
|
|
149
151
|
}
|
|
150
152
|
catch {
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
// Mock fs module
|
|
4
|
+
vi.mock('node:fs', () => ({
|
|
5
|
+
readFileSync: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
// Mock process.env
|
|
8
|
+
const originalEnv = { ...process.env };
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
// Reset process.env
|
|
12
|
+
process.env = { ...originalEnv };
|
|
13
|
+
});
|
|
14
|
+
// Import after mocking
|
|
15
|
+
const { logEnvironmentVariables } = await import('../../utils/env-logger.js');
|
|
16
|
+
describe('Environment Variable Logger', () => {
|
|
17
|
+
describe('parseEnvExample', () => {
|
|
18
|
+
it('parses simple variable with description', async () => {
|
|
19
|
+
const mockContent = `# Enable verbose logging
|
|
20
|
+
ESCRIBANO_VERBOSE=false`;
|
|
21
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
22
|
+
// Call the function indirectly via logEnvironmentVariables
|
|
23
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
24
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
25
|
+
logEnvironmentVariables();
|
|
26
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
27
|
+
const output = consoleSpy.mock.calls
|
|
28
|
+
.map((call) => call.join(' '))
|
|
29
|
+
.join('\n');
|
|
30
|
+
expect(output).toContain('ESCRIBANO_VERBOSE');
|
|
31
|
+
expect(output).toContain('Enable verbose logging');
|
|
32
|
+
consoleSpy.mockRestore();
|
|
33
|
+
});
|
|
34
|
+
it('skips section headers', async () => {
|
|
35
|
+
const mockContent = `# === Frame Extraction ===
|
|
36
|
+
# Output frame width
|
|
37
|
+
ESCRIBANO_FRAME_WIDTH=1024`;
|
|
38
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
39
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
40
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
41
|
+
logEnvironmentVariables();
|
|
42
|
+
const output = consoleSpy.mock.calls
|
|
43
|
+
.map((call) => call.join(' '))
|
|
44
|
+
.join('\n');
|
|
45
|
+
expect(output).toContain('ESCRIBANO_FRAME_WIDTH');
|
|
46
|
+
expect(output).toContain('Output frame width');
|
|
47
|
+
expect(output).not.toContain('Frame Extraction');
|
|
48
|
+
consoleSpy.mockRestore();
|
|
49
|
+
});
|
|
50
|
+
it('skips commented/deprecated variables', async () => {
|
|
51
|
+
const mockContent = `# Active variable
|
|
52
|
+
ESCRIBANO_VERBOSE=false
|
|
53
|
+
# ESCRIBANO_DEPRECATED=value`;
|
|
54
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
55
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
56
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
57
|
+
logEnvironmentVariables();
|
|
58
|
+
const output = consoleSpy.mock.calls
|
|
59
|
+
.map((call) => call.join(' '))
|
|
60
|
+
.join('\n');
|
|
61
|
+
expect(output).toContain('ESCRIBANO_VERBOSE');
|
|
62
|
+
expect(output).not.toContain('ESCRIBANO_DEPRECATED');
|
|
63
|
+
consoleSpy.mockRestore();
|
|
64
|
+
});
|
|
65
|
+
it('skips non-ESCRIBANO variables', async () => {
|
|
66
|
+
const mockContent = `ESCRIBANO_VERBOSE=false
|
|
67
|
+
OTHER_VAR=value`;
|
|
68
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
69
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
70
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
71
|
+
logEnvironmentVariables();
|
|
72
|
+
const output = consoleSpy.mock.calls
|
|
73
|
+
.map((call) => call.join(' '))
|
|
74
|
+
.join('\n');
|
|
75
|
+
expect(output).toContain('ESCRIBANO_VERBOSE');
|
|
76
|
+
expect(output).not.toContain('OTHER_VAR');
|
|
77
|
+
consoleSpy.mockRestore();
|
|
78
|
+
});
|
|
79
|
+
it('handles empty file gracefully', async () => {
|
|
80
|
+
vi.mocked(readFileSync).mockReturnValue('');
|
|
81
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
82
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
83
|
+
logEnvironmentVariables();
|
|
84
|
+
const output = consoleSpy.mock.calls
|
|
85
|
+
.map((call) => call.join(' '))
|
|
86
|
+
.join('\n');
|
|
87
|
+
expect(output).toContain('Environment Variables');
|
|
88
|
+
consoleSpy.mockRestore();
|
|
89
|
+
});
|
|
90
|
+
it('handles file not found gracefully', async () => {
|
|
91
|
+
vi.mocked(readFileSync).mockImplementation(() => {
|
|
92
|
+
throw new Error('ENOENT');
|
|
93
|
+
});
|
|
94
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
95
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
96
|
+
logEnvironmentVariables();
|
|
97
|
+
const output = consoleSpy.mock.calls
|
|
98
|
+
.map((call) => call.join(' '))
|
|
99
|
+
.join('\n');
|
|
100
|
+
expect(output).toContain('Could not parse .env.example');
|
|
101
|
+
consoleSpy.mockRestore();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('logEnvironmentVariables', () => {
|
|
105
|
+
it('does not log when ESCRIBANO_VERBOSE is false', () => {
|
|
106
|
+
process.env.ESCRIBANO_VERBOSE = 'false';
|
|
107
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
108
|
+
logEnvironmentVariables();
|
|
109
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
110
|
+
consoleSpy.mockRestore();
|
|
111
|
+
});
|
|
112
|
+
it('does not log when ESCRIBANO_VERBOSE is not set', () => {
|
|
113
|
+
delete process.env.ESCRIBANO_VERBOSE;
|
|
114
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
115
|
+
logEnvironmentVariables();
|
|
116
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
117
|
+
consoleSpy.mockRestore();
|
|
118
|
+
});
|
|
119
|
+
it('logs when ESCRIBANO_VERBOSE is true', () => {
|
|
120
|
+
const mockContent = 'ESCRIBANO_VERBOSE=false';
|
|
121
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
122
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
123
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
124
|
+
logEnvironmentVariables();
|
|
125
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
126
|
+
consoleSpy.mockRestore();
|
|
127
|
+
});
|
|
128
|
+
it('marks custom values with [CUSTOM]', () => {
|
|
129
|
+
const mockContent = `# Default batch size
|
|
130
|
+
ESCRIBANO_VLM_BATCH_SIZE=4`;
|
|
131
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
132
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
133
|
+
process.env.ESCRIBANO_VLM_BATCH_SIZE = '8';
|
|
134
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
135
|
+
logEnvironmentVariables();
|
|
136
|
+
const output = consoleSpy.mock.calls
|
|
137
|
+
.map((call) => call.join(' '))
|
|
138
|
+
.join('\n');
|
|
139
|
+
expect(output).toContain('[CUSTOM]');
|
|
140
|
+
expect(output).toContain('Current: 8');
|
|
141
|
+
expect(output).toContain('Default: 4');
|
|
142
|
+
consoleSpy.mockRestore();
|
|
143
|
+
});
|
|
144
|
+
it('does not mark default values', () => {
|
|
145
|
+
const mockContent = `# Default batch size
|
|
146
|
+
ESCRIBANO_VLM_BATCH_SIZE=4`;
|
|
147
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
148
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
149
|
+
process.env.ESCRIBANO_VLM_BATCH_SIZE = '4';
|
|
150
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
151
|
+
logEnvironmentVariables();
|
|
152
|
+
const output = consoleSpy.mock.calls
|
|
153
|
+
.map((call) => call.join(' '))
|
|
154
|
+
.join('\n');
|
|
155
|
+
expect(output).not.toContain('[CUSTOM]');
|
|
156
|
+
consoleSpy.mockRestore();
|
|
157
|
+
});
|
|
158
|
+
it('masks secret tokens', () => {
|
|
159
|
+
const mockContent = `ESCRIBANO_OUTLINE_TOKEN=`;
|
|
160
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
161
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
162
|
+
process.env.ESCRIBANO_OUTLINE_TOKEN = 'secret-api-key-123';
|
|
163
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
164
|
+
logEnvironmentVariables();
|
|
165
|
+
const output = consoleSpy.mock.calls
|
|
166
|
+
.map((call) => call.join(' '))
|
|
167
|
+
.join('\n');
|
|
168
|
+
expect(output).toContain('***');
|
|
169
|
+
expect(output).not.toContain('secret-api-key-123');
|
|
170
|
+
consoleSpy.mockRestore();
|
|
171
|
+
});
|
|
172
|
+
it('does not mask non-secret values', () => {
|
|
173
|
+
const mockContent = `ESCRIBANO_VLM_BATCH_SIZE=4`;
|
|
174
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
175
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
176
|
+
process.env.ESCRIBANO_VLM_BATCH_SIZE = '8';
|
|
177
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
178
|
+
logEnvironmentVariables();
|
|
179
|
+
const output = consoleSpy.mock.calls
|
|
180
|
+
.map((call) => call.join(' '))
|
|
181
|
+
.join('\n');
|
|
182
|
+
expect(output).toContain('8');
|
|
183
|
+
expect(output).not.toContain('***');
|
|
184
|
+
consoleSpy.mockRestore();
|
|
185
|
+
});
|
|
186
|
+
it('shows "not set" for undefined variables', () => {
|
|
187
|
+
const mockContent = `ESCRIBANO_VLM_BATCH_SIZE=4`;
|
|
188
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
189
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
190
|
+
delete process.env.ESCRIBANO_VLM_BATCH_SIZE;
|
|
191
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
192
|
+
logEnvironmentVariables();
|
|
193
|
+
const output = consoleSpy.mock.calls
|
|
194
|
+
.map((call) => call.join(' '))
|
|
195
|
+
.join('\n');
|
|
196
|
+
expect(output).toContain('not set');
|
|
197
|
+
consoleSpy.mockRestore();
|
|
198
|
+
});
|
|
199
|
+
it('shows "(empty)" for empty default values', () => {
|
|
200
|
+
const mockContent = `ESCRIBANO_OUTLINE_TOKEN=`;
|
|
201
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
202
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
203
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
204
|
+
logEnvironmentVariables();
|
|
205
|
+
const output = consoleSpy.mock.calls
|
|
206
|
+
.map((call) => call.join(' '))
|
|
207
|
+
.join('\n');
|
|
208
|
+
expect(output).toContain('(empty)');
|
|
209
|
+
consoleSpy.mockRestore();
|
|
210
|
+
});
|
|
211
|
+
it('sorts variables alphabetically', () => {
|
|
212
|
+
const mockContent = `ESCRIBANO_ZEBRA=1
|
|
213
|
+
ESCRIBANO_ALPHA=2
|
|
214
|
+
ESCRIBANO_MIDDLE=3`;
|
|
215
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
216
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
217
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
218
|
+
logEnvironmentVariables();
|
|
219
|
+
const output = consoleSpy.mock.calls
|
|
220
|
+
.map((call) => call.join(' '))
|
|
221
|
+
.join('\n');
|
|
222
|
+
const alphaIndex = output.indexOf('ESCRIBANO_ALPHA');
|
|
223
|
+
const middleIndex = output.indexOf('ESCRIBANO_MIDDLE');
|
|
224
|
+
const zebraIndex = output.indexOf('ESCRIBANO_ZEBRA');
|
|
225
|
+
expect(alphaIndex).toBeLessThan(middleIndex);
|
|
226
|
+
expect(middleIndex).toBeLessThan(zebraIndex);
|
|
227
|
+
consoleSpy.mockRestore();
|
|
228
|
+
});
|
|
229
|
+
it('includes multi-line descriptions', () => {
|
|
230
|
+
const mockContent = `# First line of description
|
|
231
|
+
# Second line of description
|
|
232
|
+
ESCRIBANO_VERBOSE=false`;
|
|
233
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
234
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
235
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
236
|
+
logEnvironmentVariables();
|
|
237
|
+
const output = consoleSpy.mock.calls
|
|
238
|
+
.map((call) => call.join(' '))
|
|
239
|
+
.join('\n');
|
|
240
|
+
expect(output).toContain('First line of description');
|
|
241
|
+
expect(output).toContain('Second line of description');
|
|
242
|
+
consoleSpy.mockRestore();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
describe('text wrapping', () => {
|
|
246
|
+
it('wraps long descriptions to fit width', () => {
|
|
247
|
+
const longDescription = 'This is a very long description that should be wrapped across multiple lines to fit within the specified width limit for better readability in the console output';
|
|
248
|
+
const mockContent = `# ${longDescription}
|
|
249
|
+
ESCRIBANO_VERBOSE=false`;
|
|
250
|
+
vi.mocked(readFileSync).mockReturnValue(mockContent);
|
|
251
|
+
process.env.ESCRIBANO_VERBOSE = 'true';
|
|
252
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
253
|
+
logEnvironmentVariables();
|
|
254
|
+
// The description should appear in the output
|
|
255
|
+
const output = consoleSpy.mock.calls
|
|
256
|
+
.map((call) => call.join(' '))
|
|
257
|
+
.join('\n');
|
|
258
|
+
expect(output).toContain('very long description');
|
|
259
|
+
consoleSpy.mockRestore();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|