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.
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Environment Variable Logger
3
+ *
4
+ * Parses .env.example to extract default values and descriptions,
5
+ * then logs all ESCRIBANO_* environment variables with comparisons
6
+ * to their defaults. Only logs when ESCRIBANO_VERBOSE=true.
7
+ */
8
+ import { readFileSync } from 'node:fs';
9
+ import { resolve } from 'node:path';
10
+ const SECRET_VARS = ['ESCRIBANO_OUTLINE_TOKEN'];
11
+ /**
12
+ * Parse .env.example file to extract variable names, defaults, and descriptions.
13
+ * Returns empty array if file not found.
14
+ */
15
+ function parseEnvExample() {
16
+ const envExamplePath = resolve(process.cwd(), '.env.example');
17
+ let content;
18
+ try {
19
+ content = readFileSync(envExamplePath, 'utf-8');
20
+ }
21
+ catch {
22
+ // File not found or unreadable
23
+ return [];
24
+ }
25
+ const vars = [];
26
+ const lines = content.split('\n');
27
+ let currentDescription = [];
28
+ for (const line of lines) {
29
+ const trimmedLine = line.trim();
30
+ // Skip empty lines - reset description
31
+ if (trimmedLine === '') {
32
+ currentDescription = [];
33
+ continue;
34
+ }
35
+ // Comment line
36
+ if (trimmedLine.startsWith('#')) {
37
+ const commentContent = trimmedLine.slice(1).trim();
38
+ // Skip section headers (pattern: # ===)
39
+ if (commentContent.startsWith('===')) {
40
+ currentDescription = [];
41
+ continue;
42
+ }
43
+ // Skip deprecated section marker
44
+ if (commentContent.toLowerCase().includes('deprecated')) {
45
+ currentDescription = [];
46
+ continue;
47
+ }
48
+ // Accumulate description
49
+ currentDescription.push(commentContent);
50
+ continue;
51
+ }
52
+ // Variable line (contains =)
53
+ if (trimmedLine.includes('=')) {
54
+ const eqIndex = trimmedLine.indexOf('=');
55
+ const name = trimmedLine.slice(0, eqIndex).trim();
56
+ const value = trimmedLine.slice(eqIndex + 1).trim();
57
+ // Skip if name starts with # (deprecated/commented)
58
+ if (name.startsWith('#')) {
59
+ currentDescription = [];
60
+ continue;
61
+ }
62
+ // Only track ESCRIBANO_* variables
63
+ if (name.startsWith('ESCRIBANO_')) {
64
+ vars.push({
65
+ name,
66
+ defaultValue: value,
67
+ description: currentDescription.join(' '),
68
+ });
69
+ }
70
+ currentDescription = [];
71
+ }
72
+ }
73
+ return vars;
74
+ }
75
+ /**
76
+ * Check if a variable should be masked (secret).
77
+ */
78
+ function isSecretVar(name) {
79
+ return SECRET_VARS.includes(name);
80
+ }
81
+ /**
82
+ * Format value for display, masking secrets if needed.
83
+ */
84
+ function formatValue(value, isSecret) {
85
+ if (value === 'not set') {
86
+ return 'not set';
87
+ }
88
+ if (isSecret && value !== 'not set' && value !== '') {
89
+ return '***';
90
+ }
91
+ if (value === '') {
92
+ return '(empty)';
93
+ }
94
+ return value;
95
+ }
96
+ /**
97
+ * Main logging function. Only runs when ESCRIBANO_VERBOSE=true.
98
+ */
99
+ export function logEnvironmentVariables() {
100
+ if (process.env.ESCRIBANO_VERBOSE !== 'true') {
101
+ return;
102
+ }
103
+ const envVars = parseEnvExample();
104
+ if (envVars.length === 0) {
105
+ console.log('\n=== Environment Variables ===');
106
+ console.log(' (Could not parse .env.example)\n');
107
+ return;
108
+ }
109
+ // Build list of vars with their current values
110
+ const varsWithValues = envVars.map((varDef) => {
111
+ const currentValue = process.env[varDef.name] ?? 'not set';
112
+ const isCustom = currentValue !== varDef.defaultValue && currentValue !== 'not set';
113
+ return {
114
+ ...varDef,
115
+ currentValue,
116
+ isCustom,
117
+ isSecret: isSecretVar(varDef.name),
118
+ };
119
+ });
120
+ // Sort alphabetically by name
121
+ varsWithValues.sort((a, b) => a.name.localeCompare(b.name));
122
+ // Log output
123
+ console.log('\n=== Environment Variables ===\n');
124
+ for (const varDef of varsWithValues) {
125
+ const marker = varDef.isCustom ? ' [CUSTOM]' : '';
126
+ const displayCurrent = formatValue(varDef.currentValue, varDef.isSecret);
127
+ const displayDefault = formatValue(varDef.defaultValue, false);
128
+ console.log(` ${varDef.name}${marker}`);
129
+ console.log(` Current: ${displayCurrent}`);
130
+ console.log(` Default: ${displayDefault}`);
131
+ if (varDef.description) {
132
+ // Wrap description to fit nicely (max ~60 chars per line)
133
+ const wrappedDesc = wrapText(varDef.description, 58);
134
+ for (const line of wrappedDesc) {
135
+ console.log(` ${line}`);
136
+ }
137
+ }
138
+ console.log('');
139
+ }
140
+ }
141
+ /**
142
+ * Wrap text to specified width.
143
+ */
144
+ function wrapText(text, width) {
145
+ if (text.length <= width) {
146
+ return [text];
147
+ }
148
+ const words = text.split(' ');
149
+ const lines = [];
150
+ let currentLine = '';
151
+ for (const word of words) {
152
+ if (`${currentLine} ${word}`.trim().length <= width) {
153
+ currentLine = currentLine ? `${currentLine} ${word}` : word;
154
+ }
155
+ else {
156
+ if (currentLine) {
157
+ lines.push(currentLine);
158
+ }
159
+ currentLine = word;
160
+ }
161
+ }
162
+ if (currentLine) {
163
+ lines.push(currentLine);
164
+ }
165
+ return lines;
166
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "escribano",
3
- "version": "0.1.4",
3
+ "version": "0.2.2",
4
4
  "description": "AI-powered session intelligence tool — turn screen recordings into structured work summaries",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -11,7 +11,9 @@
11
11
  "dist",
12
12
  "migrations",
13
13
  "prompts",
14
- "scripts"
14
+ "scripts",
15
+ "src/scripts/audio_preprocessor.py",
16
+ "src/scripts/visual_observer_base.py"
15
17
  ],
16
18
  "scripts": {
17
19
  "test": "vitest run",
@@ -20,6 +22,7 @@
20
22
  "build": "tsc",
21
23
  "postbuild": "node scripts/add-shebang.mjs",
22
24
  "prepublishOnly": "pnpm build",
25
+ "postpublish": "node scripts/create-release.mjs",
23
26
  "lint": "biome check .",
24
27
  "lint:fix": "biome check --write .",
25
28
  "format": "biome format --write .",
@@ -30,7 +33,7 @@
30
33
  "dashboard": "node tools/dashboard/server.js",
31
34
  "db:reset": "rm -f ~/.escribano/escribano.db*",
32
35
  "ollama": "OLLAMA_NUM_PARALLEL=4 OLLAMA_MAX_LOADED_MODELS=3 OLLAMA_FLASH_ATTENTION=1 OLLAMA_KEEP_ALIVE=-1 OLLAMA_CONTEXT_LENGTH=262144 ollama serve",
33
- "ollama-2": "OLLAMA_NUM_PARALLEL=1 OLLAMA_HOST=127.0.0.1:11435 OLLAMA_MAX_LOADED_MODELS=1 OLLAMA_FLASH_ATTENTION=1 OLLAMA_KEEP_ALIVE=-1 OLLAMA_CONTEXT_LENGTH=262144 ollama serve",
36
+ "ollama-2": "OLLAMA_NUM_PARALLEL=1 OLLAMA_HOST=127.0.0.1.11435 OLLAMA_MAX_LOADED_MODELS=1 OLLAMA_FLASH_ATTENTION=1 OLLAMA_KEEP_ALIVE=-1 OLLAMA_CONTEXT_LENGTH=262144 ollama serve",
34
37
  "index:rebuild": "tsx --env-file=.env src/scripts/rebuild-index.ts"
35
38
  },
36
39
  "keywords": [
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
5
+ import { join } from "node:path";
6
+
7
+ const REPO_ROOT = join(import.meta.dirname, "..");
8
+ const CHANGELOG_PATH = join(REPO_ROOT, "CHANGELOG.md");
9
+
10
+ function run(cmd) {
11
+ return execSync(cmd, { encoding: "utf-8", cwd: REPO_ROOT }).trim();
12
+ }
13
+
14
+ function getPreviousTag(currentTag, allTags) {
15
+ const currentIndex = allTags.indexOf(currentTag);
16
+ if (currentIndex === 0) return null;
17
+ return allTags[currentIndex - 1];
18
+ }
19
+
20
+ function getCommitsBetweenTags(fromTag, toTag) {
21
+ const range = fromTag ? `${fromTag}..${toTag}` : toTag;
22
+ const format = "%H|%s|%an|%ad";
23
+ const dateformat = "--date=short";
24
+
25
+ const commits = run(`git log ${range} --pretty=format:'${format}' ${dateformat}`)
26
+ .split("\n")
27
+ .filter(Boolean)
28
+ .map(line => {
29
+ const [hash, subject, author, date] = line.split("|");
30
+ return { hash, subject, author, date };
31
+ });
32
+
33
+ return commits;
34
+ }
35
+
36
+ async function generateReleaseNotes(commits, version, tagDate) {
37
+ const commitList = commits
38
+ .map(c => `- ${c.subject} (${c.author}, ${c.date})`)
39
+ .join("\n");
40
+
41
+ const prompt = `You are a release notes generator. Given these git commits, create a clean, user-friendly changelog entry.
42
+
43
+ Version: ${version}
44
+ Date: ${tagDate}
45
+
46
+ Commits:
47
+ ${commitList}
48
+
49
+ Generate a changelog entry in this exact format (use markdown):
50
+
51
+ ## [${version}] - ${tagDate}
52
+
53
+ ### Added
54
+ - (new features)
55
+
56
+ ### Fixed
57
+ - (bug fixes)
58
+
59
+ ### Changed
60
+ - (improvements, refactors)
61
+
62
+ ### Removed
63
+ - (deprecated features removed)
64
+
65
+ Rules:
66
+ - Group commits into the most appropriate section
67
+ - Use present tense ("Add feature" not "Added feature")
68
+ - Be concise but descriptive
69
+ - If a section has no commits, omit it entirely
70
+ - Only output the changelog entry, no additional text`;
71
+
72
+ try {
73
+ const response = await fetch("http://localhost:11434/api/generate", {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/json" },
76
+ body: JSON.stringify({
77
+ model: process.env.ESCRIBANO_LLM_MODEL || "qwen3:8b",
78
+ prompt,
79
+ stream: false,
80
+ options: {
81
+ temperature: 0.3,
82
+ },
83
+ }),
84
+ });
85
+
86
+ if (!response.ok) {
87
+ throw new Error(`Ollama request failed: ${response.statusText}`);
88
+ }
89
+
90
+ const data = await response.json();
91
+ return data.response.trim();
92
+ } catch (error) {
93
+ console.error("⚠️ Failed to generate notes with Ollama, using fallback");
94
+ console.error(error.message);
95
+
96
+ const changes = commits.map(c => `- ${c.subject}`).join("\n");
97
+
98
+ return `## [${version}] - ${tagDate}
99
+
100
+ ### Changed
101
+ ${changes}`;
102
+ }
103
+ }
104
+
105
+ function updateChangelog(releaseNotes) {
106
+ let changelog = "";
107
+
108
+ if (existsSync(CHANGELOG_PATH)) {
109
+ changelog = readFileSync(CHANGELOG_PATH, "utf-8");
110
+ }
111
+
112
+ const header = `# Changelog
113
+
114
+ All notable changes to this project will be documented in this file.
115
+
116
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
117
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
118
+
119
+ `;
120
+
121
+ if (!changelog.includes("# Changelog")) {
122
+ changelog = header + changelog;
123
+ }
124
+
125
+ // Insert new release after header
126
+ const lines = changelog.split("\n");
127
+ const insertIndex = lines.findIndex(line => line.startsWith("## [")) || 6;
128
+
129
+ lines.splice(insertIndex, 0, releaseNotes, "");
130
+
131
+ writeFileSync(CHANGELOG_PATH, lines.join("\n"));
132
+ console.log("✅ Updated CHANGELOG.md");
133
+ }
134
+
135
+ function createGitHubRelease(tag, releaseNotes) {
136
+ const notesFile = `/tmp/escribano-release-${tag}.md`;
137
+ writeFileSync(notesFile, releaseNotes);
138
+
139
+ try {
140
+ run(`gh release create ${tag} --title "${tag}" --notes-file "${notesFile}"`);
141
+ console.log(`✅ Created GitHub release: ${tag}`);
142
+ } catch (error) {
143
+ if (error.message.includes("already exists")) {
144
+ console.log(`⚠️ Release ${tag} already exists, skipping`);
145
+ } else {
146
+ throw error;
147
+ }
148
+ }
149
+ }
150
+
151
+ async function main() {
152
+ const args = process.argv.slice(2);
153
+
154
+ if (args.length === 0) {
155
+ console.log("Usage: node scripts/backfill-releases.mjs <tag1> [tag2] [tag3] ...");
156
+ console.log("Example: node scripts/backfill-releases.mjs v0.1.1 v0.1.3 v0.2.0");
157
+ process.exit(1);
158
+ }
159
+
160
+ const tagsToProcess = args;
161
+ const allTags = run("git tag --list 'v*' | sort -V").split("\n").filter(Boolean);
162
+
163
+ console.log(`🚀 Creating releases for ${tagsToProcess.length} tags...\n`);
164
+
165
+ for (const tag of tagsToProcess) {
166
+ if (!allTags.includes(tag)) {
167
+ console.error(`❌ Tag ${tag} not found`);
168
+ continue;
169
+ }
170
+
171
+ console.log(`\n📌 Processing ${tag}`);
172
+
173
+ const previousTag = getPreviousTag(tag, allTags);
174
+ console.log(` Previous tag: ${previousTag || "none (first release)"}`);
175
+
176
+ const commits = getCommitsBetweenTags(previousTag, tag);
177
+ console.log(` Found ${commits.length} commits`);
178
+
179
+ if (commits.length === 0) {
180
+ console.log(" ⚠️ No commits found, skipping");
181
+ continue;
182
+ }
183
+
184
+ const version = tag.replace(/^v/, "");
185
+ const tagDate = run(`git log -1 --format=%ad --date=short ${tag}`);
186
+
187
+ const releaseNotes = await generateReleaseNotes(commits, version, tagDate);
188
+
189
+ console.log("\n Generated release notes:");
190
+ console.log(" " + releaseNotes.split("\n").join("\n "));
191
+ console.log("\n ---");
192
+
193
+ updateChangelog(releaseNotes);
194
+ createGitHubRelease(tag, releaseNotes);
195
+ }
196
+
197
+ console.log("\n🎉 All releases complete!");
198
+ console.log("\nNext steps:");
199
+ console.log(" 1. Review CHANGELOG.md changes");
200
+ console.log(" 2. Commit: git add CHANGELOG.md && git commit -m 'docs: add CHANGELOG'");
201
+ console.log(" 3. Push: git push");
202
+ }
203
+
204
+ main().catch(error => {
205
+ console.error("❌ Release creation failed:", error.message);
206
+ process.exit(1);
207
+ });
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
5
+ import { join } from "node:path";
6
+
7
+ const REPO_ROOT = join(import.meta.dirname, "..");
8
+ const CHANGELOG_PATH = join(REPO_ROOT, "CHANGELOG.md");
9
+
10
+ function run(cmd) {
11
+ return execSync(cmd, { encoding: "utf-8", cwd: REPO_ROOT }).trim();
12
+ }
13
+
14
+ function getCurrentTag() {
15
+ try {
16
+ return run("git describe --tags --exact-match");
17
+ } catch {
18
+ console.error("❌ No git tag found at current commit");
19
+ console.error(" Run: git tag v<x.y.z> && git push --tags");
20
+ process.exit(1);
21
+ }
22
+ }
23
+
24
+ function getPreviousTag(currentTag) {
25
+ const allTags = run("git tag --list 'v*' | sort -V").split("\n").filter(Boolean);
26
+ const currentIndex = allTags.indexOf(currentTag);
27
+
28
+ if (currentIndex === 0) return null;
29
+ return allTags[currentIndex - 1];
30
+ }
31
+
32
+ function getCommitsSinceTag(previousTag, currentTag) {
33
+ const range = previousTag ? `${previousTag}..${currentTag}` : currentTag;
34
+ const format = "%H|%s|%an|%ad";
35
+ const dateformat = "--date=short";
36
+
37
+ const commits = run(`git log ${range} --pretty=format:'${format}' ${dateformat}`)
38
+ .split("\n")
39
+ .filter(Boolean)
40
+ .map(line => {
41
+ const [hash, subject, author, date] = line.split("|");
42
+ return { hash, subject, author, date };
43
+ });
44
+
45
+ return commits;
46
+ }
47
+
48
+ async function generateReleaseNotes(commits, version) {
49
+ const commitList = commits
50
+ .map(c => `- ${c.subject} (${c.author}, ${c.date})`)
51
+ .join("\n");
52
+
53
+ const prompt = `You are a release notes generator. Given these git commits, create a clean, user-friendly changelog entry.
54
+
55
+ Version: ${version}
56
+
57
+ Commits:
58
+ ${commitList}
59
+
60
+ Generate a changelog entry in this exact format (use markdown):
61
+
62
+ ## [${version}] - ${new Date().toISOString().split("T")[0]}
63
+
64
+ ### Added
65
+ - (new features)
66
+
67
+ ### Fixed
68
+ - (bug fixes)
69
+
70
+ ### Changed
71
+ - (improvements, refactors)
72
+
73
+ ### Removed
74
+ - (deprecated features removed)
75
+
76
+ Rules:
77
+ - Group commits into the most appropriate section
78
+ - Use present tense ("Add feature" not "Added feature")
79
+ - Be concise but descriptive
80
+ - If a section has no commits, omit it entirely
81
+ - Only output the changelog entry, no additional text`;
82
+
83
+ try {
84
+ const response = await fetch("http://localhost:11434/api/generate", {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify({
88
+ model: process.env.ESCRIBANO_LLM_MODEL || "qwen3:8b",
89
+ prompt,
90
+ stream: false,
91
+ options: {
92
+ temperature: 0.3,
93
+ },
94
+ }),
95
+ });
96
+
97
+ if (!response.ok) {
98
+ throw new Error(`Ollama request failed: ${response.statusText}`);
99
+ }
100
+
101
+ const data = await response.json();
102
+ return data.response.trim();
103
+ } catch (error) {
104
+ console.error("⚠️ Failed to generate notes with Ollama, using fallback");
105
+ console.error(error.message);
106
+
107
+ // Fallback: simple format
108
+ const date = new Date().toISOString().split("T")[0];
109
+ const changes = commits.map(c => `- ${c.subject}`).join("\n");
110
+
111
+ return `## [${version}] - ${date}
112
+
113
+ ### Changed
114
+ ${changes}`;
115
+ }
116
+ }
117
+
118
+ function updateChangelog(releaseNotes) {
119
+ let changelog = "";
120
+
121
+ if (existsSync(CHANGELOG_PATH)) {
122
+ changelog = readFileSync(CHANGELOG_PATH, "utf-8");
123
+ }
124
+
125
+ const header = `# Changelog
126
+
127
+ All notable changes to this project will be documented in this file.
128
+
129
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
130
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
131
+
132
+ `;
133
+
134
+ if (!changelog.includes("# Changelog")) {
135
+ changelog = header + changelog;
136
+ }
137
+
138
+ // Insert new release after header
139
+ const lines = changelog.split("\n");
140
+ const insertIndex = lines.findIndex(line => line.startsWith("## [")) || 6;
141
+
142
+ lines.splice(insertIndex, 0, releaseNotes, "");
143
+
144
+ writeFileSync(CHANGELOG_PATH, lines.join("\n"));
145
+ console.log("✅ Updated CHANGELOG.md");
146
+ }
147
+
148
+ function createGitHubRelease(tag, releaseNotes) {
149
+ const notesFile = `/tmp/escribano-release-${tag}.md`;
150
+ writeFileSync(notesFile, releaseNotes);
151
+
152
+ try {
153
+ run(`gh release create ${tag} --title "${tag}" --notes-file "${notesFile}"`);
154
+ console.log(`✅ Created GitHub release: ${tag}`);
155
+ } catch (error) {
156
+ if (error.message.includes("already exists")) {
157
+ console.log(`⚠️ Release ${tag} already exists, skipping`);
158
+ } else {
159
+ throw error;
160
+ }
161
+ }
162
+ }
163
+
164
+ async function main() {
165
+ console.log("🚀 Creating GitHub release...\n");
166
+
167
+ const currentTag = getCurrentTag();
168
+ console.log(`📌 Current tag: ${currentTag}`);
169
+
170
+ const previousTag = getPreviousTag(currentTag);
171
+ console.log(`📌 Previous tag: ${previousTag || "none (first release)"}`);
172
+
173
+ const commits = getCommitsSinceTag(previousTag, currentTag);
174
+ console.log(`📝 Found ${commits.length} commits\n`);
175
+
176
+ if (commits.length === 0) {
177
+ console.log("⚠️ No commits found, skipping release creation");
178
+ process.exit(0);
179
+ }
180
+
181
+ const version = currentTag.replace(/^v/, "");
182
+ const releaseNotes = await generateReleaseNotes(commits, version);
183
+
184
+ console.log("Generated release notes:\n");
185
+ console.log(releaseNotes);
186
+ console.log("\n---\n");
187
+
188
+ updateChangelog(releaseNotes);
189
+ createGitHubRelease(currentTag, releaseNotes);
190
+
191
+ console.log("\n🎉 Release complete!");
192
+ console.log("\nNext steps:");
193
+ console.log(" 1. Review CHANGELOG.md changes");
194
+ console.log(" 2. Commit: git add CHANGELOG.md && git commit -m 'docs: update CHANGELOG'");
195
+ console.log(" 3. Push: git push");
196
+ }
197
+
198
+ main().catch(error => {
199
+ console.error("❌ Release creation failed:", error.message);
200
+ process.exit(1);
201
+ });