listener-ai 2.5.0 → 2.5.1

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/README.md CHANGED
@@ -41,8 +41,13 @@ listener config set notionDatabaseId <your-id>
41
41
  ```bash
42
42
  listener recording.mp3 # Transcribe to default output dir
43
43
  listener recording.m4a --output ./ # Transcribe to current directory
44
- listener config list # Show all config values
44
+ listener config list # Show all config values (secrets masked)
45
+ listener config get <key> # Print one config value
46
+ listener config set <key> <value> # Set a config value
47
+ listener config unset <key> # Clear a config value (falls back to default)
45
48
  listener config path # Print config file path
49
+ listener --version # Print CLI version
50
+ listener --help # Show usage
46
51
  ```
47
52
 
48
53
  Supported formats: mp3, m4a, wav, ogg, flac, aac, wma, opus, webm
package/dist/cli.js CHANGED
@@ -57,32 +57,49 @@ const SUPPORTED_EXTENSIONS = new Set([
57
57
  '.opus',
58
58
  '.webm',
59
59
  ]);
60
- function usage() {
61
- process.stderr.write('Usage: listener <file> [--output <dir>] Transcribe an audio file\n' +
62
- ' listener list [--limit <n>] List past transcriptions\n' +
63
- ' listener show <ref> Print summary to stdout\n' +
64
- ' listener export <ref> [<path>] [--json] [--transcript]\n' +
65
- ' Export transcription\n' +
66
- ' listener search <query> [--limit <n>] [--transcript] [--field <name>]\n' +
67
- ' Search past transcriptions\n' +
68
- ' listener merge <ref1> <ref2> [<ref3>...] [--title <t>]\n' +
69
- ' Concat the source audio of two or more notes,\n' +
70
- ' re-transcribe end-to-end, and save as a new note\n' +
71
- ' listener ask <question> [--ref <ref>]\n' +
72
- ' Ask the AI agent about saved meetings or settings\n' +
73
- ' listener config list|get|set|path Manage configuration\n' +
74
- '\n' +
75
- '<ref> is a number from `listener list` or a folder name.\n' +
76
- '\n' +
77
- 'Options:\n' +
78
- ' --output <dir> Parent directory for the output folder\n' +
79
- ' --limit <n> Max results (0 = all, default: 20)\n' +
80
- ' --json Export as JSON instead of markdown\n' +
81
- ' --transcript Include transcript body (export: append; search: widen scope)\n' +
82
- ' --field <name> Restrict search to one of: title, summary, keyPoints, actionItems, transcript, all\n' +
83
- ' --help Show this help message\n');
60
+ const VERSION = (() => {
61
+ try {
62
+ const pkgPath = path.join(__dirname, '..', 'package.json');
63
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
64
+ return pkg.version ?? 'unknown';
65
+ }
66
+ catch {
67
+ return 'unknown';
68
+ }
69
+ })();
70
+ const USAGE_TEXT = 'Usage: listener <file> [--output <dir>] Transcribe an audio file\n' +
71
+ ' listener list [--limit <n>] List past transcriptions\n' +
72
+ ' listener show <ref> Print summary to stdout\n' +
73
+ ' listener export <ref> [<path>] [--json] [--transcript]\n' +
74
+ ' Export transcription\n' +
75
+ ' listener search <query> [--limit <n>] [--transcript] [--field <name>]\n' +
76
+ ' Search past transcriptions\n' +
77
+ ' listener merge <ref1> <ref2> [<ref3>...] [--title <t>]\n' +
78
+ ' Concat the source audio of two or more notes,\n' +
79
+ ' re-transcribe end-to-end, and save as a new note\n' +
80
+ ' listener ask <question> [--ref <ref>]\n' +
81
+ ' Ask the AI agent about saved meetings or settings\n' +
82
+ ' listener config list|get|set|unset|path\n' +
83
+ ' Manage configuration\n' +
84
+ '\n' +
85
+ '<ref> is a number from `listener list` or a folder name.\n' +
86
+ '\n' +
87
+ 'Options:\n' +
88
+ ' --output <dir> Parent directory for the output folder\n' +
89
+ ' --limit <n> Max results (0 = all, default: 20)\n' +
90
+ ' --json Export as JSON instead of markdown\n' +
91
+ ' --transcript Include transcript body (export: append; search: widen scope)\n' +
92
+ ' --field <name> Restrict search to one of: title, summary, keyPoints, actionItems, transcript, all\n' +
93
+ ' --version, -V Print CLI version\n' +
94
+ ' --help, -h Show this help message\n';
95
+ function usageError() {
96
+ process.stderr.write(USAGE_TEXT);
84
97
  process.exit(1);
85
98
  }
99
+ function showHelp() {
100
+ process.stdout.write(USAGE_TEXT);
101
+ process.exit(0);
102
+ }
86
103
  const KNOWN_CONFIG_KEYS = [
87
104
  'geminiApiKey',
88
105
  'geminiModel',
@@ -90,23 +107,124 @@ const KNOWN_CONFIG_KEYS = [
90
107
  'notionApiKey',
91
108
  'notionDatabaseId',
92
109
  'autoMode',
110
+ 'meetingDetection',
111
+ 'displayDetection',
93
112
  'globalShortcut',
113
+ 'knownWords',
114
+ 'summaryPrompt',
115
+ 'maxRecordingMinutes',
116
+ 'recordingReminderMinutes',
94
117
  'minRecordingSeconds',
118
+ 'recordSystemAudio',
119
+ 'slackWebhookUrl',
120
+ 'slackAutoShare',
95
121
  ];
122
+ function isSensitiveKey(key) {
123
+ const lk = key.toLowerCase();
124
+ return lk.includes('key') || lk.includes('webhook');
125
+ }
96
126
  function maskValue(key, value) {
97
127
  if (value == null || value === '')
98
128
  return '(not set)';
99
- if (key.toLowerCase().includes('key')) {
100
- return value.length > 4 ? `****${value.slice(-4)}` : '****';
129
+ if (Array.isArray(value)) {
130
+ if (value.length === 0)
131
+ return '(none)';
132
+ const joined = value.map((x) => String(x)).join(', ');
133
+ return joined.length > 60 ? `${joined.slice(0, 57)}...` : joined;
134
+ }
135
+ const str = String(value);
136
+ if (isSensitiveKey(key)) {
137
+ return str.length > 4 ? `****${str.slice(-4)}` : '****';
138
+ }
139
+ if (str.length > 60)
140
+ return `${str.slice(0, 57)}...`;
141
+ return str;
142
+ }
143
+ function parseBool(key, v) {
144
+ if (v !== 'true' && v !== 'false') {
145
+ process.stderr.write(`Error: ${key} must be "true" or "false"\n`);
146
+ process.exit(1);
147
+ }
148
+ return v === 'true';
149
+ }
150
+ function parseNonNegInt(key, v) {
151
+ const n = Number.parseInt(v, 10);
152
+ if (Number.isNaN(n) || n < 0 || String(n) !== v.trim()) {
153
+ process.stderr.write(`Error: ${key} must be a non-negative integer\n`);
154
+ process.exit(1);
155
+ }
156
+ return n;
157
+ }
158
+ function parseKnownWords(v) {
159
+ return v
160
+ .split(',')
161
+ .map((s) => s.trim())
162
+ .filter((s) => s.length > 0);
163
+ }
164
+ function applyConfigSet(config, key, value) {
165
+ switch (key) {
166
+ case 'geminiApiKey':
167
+ config.setGeminiApiKey(value);
168
+ return;
169
+ case 'geminiModel':
170
+ config.setGeminiModel(value);
171
+ return;
172
+ case 'geminiFlashModel':
173
+ config.setGeminiFlashModel(value);
174
+ return;
175
+ case 'notionApiKey':
176
+ config.setNotionApiKey(value);
177
+ return;
178
+ case 'notionDatabaseId':
179
+ config.setNotionDatabaseId(value);
180
+ return;
181
+ case 'autoMode':
182
+ config.setAutoMode(parseBool('autoMode', value));
183
+ return;
184
+ case 'meetingDetection':
185
+ config.updateConfig({ meetingDetection: parseBool('meetingDetection', value) });
186
+ return;
187
+ case 'displayDetection':
188
+ config.setDisplayDetection(parseBool('displayDetection', value));
189
+ return;
190
+ case 'globalShortcut':
191
+ config.setGlobalShortcut(value);
192
+ return;
193
+ case 'knownWords':
194
+ config.setKnownWords(parseKnownWords(value));
195
+ return;
196
+ case 'summaryPrompt':
197
+ config.setSummaryPrompt(value);
198
+ return;
199
+ case 'maxRecordingMinutes':
200
+ config.setMaxRecordingMinutes(parseNonNegInt('maxRecordingMinutes', value));
201
+ return;
202
+ case 'recordingReminderMinutes':
203
+ config.setRecordingReminderMinutes(parseNonNegInt('recordingReminderMinutes', value));
204
+ return;
205
+ case 'minRecordingSeconds':
206
+ config.setMinRecordingSeconds(parseNonNegInt('minRecordingSeconds', value));
207
+ return;
208
+ case 'recordSystemAudio':
209
+ config.setRecordSystemAudio(parseBool('recordSystemAudio', value));
210
+ return;
211
+ case 'slackWebhookUrl':
212
+ config.setSlackWebhookUrl(value);
213
+ return;
214
+ case 'slackAutoShare':
215
+ config.setSlackAutoShare(parseBool('slackAutoShare', value));
216
+ return;
101
217
  }
102
- return value;
103
218
  }
104
219
  function handleConfig(subArgs) {
105
220
  const dataPath = (0, dataPath_1.getDataPath)();
106
221
  const config = new configService_1.ConfigService(dataPath);
107
222
  const sub = subArgs[0];
108
- if (!sub || sub === '--help') {
109
- usage();
223
+ if (!sub) {
224
+ usageError();
225
+ }
226
+ if (sub === '--help' || sub === '-h') {
227
+ showHelp();
110
228
  }
111
229
  if (sub === 'path') {
112
230
  process.stdout.write(`${config.getConfigPath()}\n`);
@@ -116,8 +234,7 @@ function handleConfig(subArgs) {
116
234
  const all = config.getAllConfig();
117
235
  for (const key of KNOWN_CONFIG_KEYS) {
118
236
  const raw = all[key];
119
- const display = maskValue(key, raw == null ? undefined : String(raw));
120
- process.stdout.write(`${key}=${display}\n`);
237
+ process.stdout.write(`${key}=${maskValue(key, raw)}\n`);
121
238
  }
122
239
  return;
123
240
  }
@@ -134,7 +251,12 @@ function handleConfig(subArgs) {
134
251
  }
135
252
  const all = config.getAllConfig();
136
253
  const val = all[key];
137
- process.stdout.write(`${val ?? ''}\n`);
254
+ if (Array.isArray(val)) {
255
+ process.stdout.write(`${val.join(',')}\n`);
256
+ }
257
+ else {
258
+ process.stdout.write(`${val ?? ''}\n`);
259
+ }
138
260
  return;
139
261
  }
140
262
  if (sub === 'set') {
@@ -149,35 +271,27 @@ function handleConfig(subArgs) {
149
271
  process.stderr.write(`Known keys: ${KNOWN_CONFIG_KEYS.join(', ')}\n`);
150
272
  process.exit(1);
151
273
  }
152
- const setters = {
153
- geminiApiKey: (v) => config.setGeminiApiKey(v),
154
- geminiModel: (v) => config.setGeminiModel(v),
155
- geminiFlashModel: (v) => config.setGeminiFlashModel(v),
156
- notionApiKey: (v) => config.setNotionApiKey(v),
157
- notionDatabaseId: (v) => config.setNotionDatabaseId(v),
158
- autoMode: (v) => {
159
- if (v !== 'true' && v !== 'false') {
160
- process.stderr.write('Error: autoMode must be "true" or "false"\n');
161
- process.exit(1);
162
- }
163
- config.setAutoMode(v === 'true');
164
- },
165
- globalShortcut: (v) => config.setGlobalShortcut(v),
166
- minRecordingSeconds: (v) => {
167
- const n = Number.parseInt(v, 10);
168
- if (Number.isNaN(n) || n < 0 || String(n) !== v.trim()) {
169
- process.stderr.write('Error: minRecordingSeconds must be a non-negative integer (0 disables)\n');
170
- process.exit(1);
171
- }
172
- config.setMinRecordingSeconds(n);
173
- },
174
- };
175
- setters[key](value);
274
+ applyConfigSet(config, key, value);
176
275
  process.stderr.write(`Set ${key}\n`);
177
276
  return;
178
277
  }
278
+ if (sub === 'unset') {
279
+ const key = subArgs[1];
280
+ if (!key) {
281
+ process.stderr.write('Error: Missing key. Usage: listener config unset <key>\n');
282
+ process.exit(1);
283
+ }
284
+ if (!KNOWN_CONFIG_KEYS.includes(key)) {
285
+ process.stderr.write(`Error: Unknown key: ${key}\n`);
286
+ process.stderr.write(`Known keys: ${KNOWN_CONFIG_KEYS.join(', ')}\n`);
287
+ process.exit(1);
288
+ }
289
+ config.unsetKey(key);
290
+ process.stderr.write(`Unset ${key}\n`);
291
+ return;
292
+ }
179
293
  process.stderr.write(`Error: Unknown config command: ${sub}\n`);
180
- usage();
294
+ usageError();
181
295
  }
182
296
  async function resolveRef(ref, dataPath) {
183
297
  if (/^\d+$/.test(ref)) {
@@ -550,8 +664,15 @@ async function handleAsk(args) {
550
664
  }
551
665
  async function main() {
552
666
  const args = process.argv.slice(2);
553
- if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
554
- usage();
667
+ if (args.includes('--version') || args.includes('-V')) {
668
+ process.stdout.write(`listener ${VERSION}\n`);
669
+ return;
670
+ }
671
+ if (args.includes('--help') || args.includes('-h')) {
672
+ showHelp();
673
+ }
674
+ if (args.length === 0) {
675
+ usageError();
555
676
  }
556
677
  if (args[0] === 'config') {
557
678
  handleConfig(args.slice(1));
@@ -590,7 +711,7 @@ async function main() {
590
711
  }
591
712
  else if (args[i].startsWith('-')) {
592
713
  process.stderr.write(`Error: Unknown option: ${args[i]}\n`);
593
- usage();
714
+ usageError();
594
715
  }
595
716
  else {
596
717
  filePath = args[i];
@@ -598,7 +719,7 @@ async function main() {
598
719
  }
599
720
  if (!filePath) {
600
721
  process.stderr.write('Error: No audio file specified.\n');
601
- usage();
722
+ usageError();
602
723
  }
603
724
  // Resolve to absolute path
604
725
  filePath = path.resolve(filePath);
@@ -240,6 +240,10 @@ class ConfigService {
240
240
  }
241
241
  this.saveConfig();
242
242
  }
243
+ unsetKey(key) {
244
+ delete this.config[key];
245
+ this.saveConfig();
246
+ }
243
247
  getAllConfig() {
244
248
  return {
245
249
  geminiApiKey: this.getGeminiApiKey(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "listener-ai",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "A lightweight desktop application for recording and transcribing meetings with AI-powered notes.",
5
5
  "main": "dist/main.js",
6
6
  "bin": {
@@ -25,9 +25,10 @@
25
25
  "build:renderer": "vite build",
26
26
  "build:check-renderer": "tsc -p tsconfig.renderer.json",
27
27
  "dev:renderer": "vite",
28
- "lint": "biome check .",
29
- "lint:fix": "biome check --write .",
30
- "format": "biome format --write .",
28
+ "lint": "oxlint src renderer scripts",
29
+ "lint:fix": "oxlint --fix src renderer scripts",
30
+ "format": "oxfmt 'src/**/*.ts' 'renderer/**/*.ts' 'renderer/**/*.tsx' 'scripts/**/*.js' 'scripts/**/*.ts'",
31
+ "format:check": "oxfmt --check 'src/**/*.ts' 'renderer/**/*.ts' 'renderer/**/*.tsx' 'scripts/**/*.js' 'scripts/**/*.ts'",
31
32
  "test": "tsc && node --test dist/meetingDetectorService.test.js dist/searchService.test.js dist/agentService.test.js dist/simpleAudioRecorder.test.js dist/outputService.test.js dist/slackService.test.js dist/services/audioConcatService.test.js dist/cli.test.js",
32
33
  "dist": "pnpm run build && electron-builder",
33
34
  "dist:mac": "pnpm run build && electron-builder --mac",
@@ -53,14 +54,15 @@
53
54
  },
54
55
  "homepage": "https://github.com/asleep-ai/listener-ai#readme",
55
56
  "devDependencies": {
56
- "@biomejs/biome": "^1.9.4",
57
57
  "@electron/notarize": "^3.0.1",
58
58
  "@types/fs-extra": "^11.0.4",
59
59
  "@types/node": "^24.0.12",
60
60
  "dotenv": "^17.2.0",
61
61
  "electron": "^39.8.5",
62
- "vite": "^5.4.10",
62
+ "vite": "^6.4.2",
63
63
  "electron-builder": "^26.8.1",
64
+ "oxfmt": "^0.48.0",
65
+ "oxlint": "^1.63.0",
64
66
  "semver": "^7.7.2",
65
67
  "typescript": "^5.8.3"
66
68
  },