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 +6 -1
- package/dist/cli.js +182 -61
- package/dist/configService.js +4 -0
- package/package.json +8 -6
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 (
|
|
100
|
-
|
|
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
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
554
|
-
|
|
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
|
-
|
|
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
|
-
|
|
722
|
+
usageError();
|
|
602
723
|
}
|
|
603
724
|
// Resolve to absolute path
|
|
604
725
|
filePath = path.resolve(filePath);
|
package/dist/configService.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "listener-ai",
|
|
3
|
-
"version": "2.5.
|
|
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": "
|
|
29
|
-
"lint:fix": "
|
|
30
|
-
"format": "
|
|
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": "^
|
|
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
|
},
|