listener-ai 2.6.0 → 2.7.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 +87 -22
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/dist/agentService.js +142 -119
- package/dist/aiProvider.js +35 -0
- package/dist/cli.js +119 -38
- package/dist/codexOAuth.js +68 -0
- package/dist/codexOAuthHolder.js +26 -0
- package/dist/codexTranscription.js +168 -0
- package/dist/configService.js +171 -25
- package/dist/dataPath.js +30 -10
- package/dist/esmImport.js +15 -0
- package/dist/geminiService.js +203 -39
- package/dist/main.js +84 -17
- package/dist/piAiClient.js +102 -0
- package/package.json +13 -4
package/dist/configService.js
CHANGED
|
@@ -36,6 +36,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.ConfigService = exports.DEFAULT_SUMMARY_PROMPT = void 0;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
+
const aiProvider_1 = require("./aiProvider");
|
|
40
|
+
const codexOAuth_1 = require("./codexOAuth");
|
|
39
41
|
exports.DEFAULT_SUMMARY_PROMPT = `Based on this meeting transcript, provide:
|
|
40
42
|
|
|
41
43
|
1. A concise meeting title in Korean (10-20 characters that captures the main topic)
|
|
@@ -58,6 +60,12 @@ class ConfigService {
|
|
|
58
60
|
}
|
|
59
61
|
constructor(dataPath) {
|
|
60
62
|
this.config = {};
|
|
63
|
+
// Keys this process has explicitly modified since the last successful save.
|
|
64
|
+
// saveConfig() re-reads the file on every write and applies only these keys on
|
|
65
|
+
// top of disk state, so a concurrent process (Electron app + CLI hitting the
|
|
66
|
+
// same config.json during OAuth refresh, etc.) cannot clobber unrelated keys.
|
|
67
|
+
this.dirtyKeys = new Set();
|
|
68
|
+
this.envProviderWarned = false;
|
|
61
69
|
let userDataPath;
|
|
62
70
|
if (dataPath) {
|
|
63
71
|
userDataPath = dataPath;
|
|
@@ -72,6 +80,31 @@ class ConfigService {
|
|
|
72
80
|
}
|
|
73
81
|
this.configPath = path.join(userDataPath, 'config.json');
|
|
74
82
|
this.loadConfig();
|
|
83
|
+
this.migrateLegacyDefaults();
|
|
84
|
+
}
|
|
85
|
+
// One-shot upgrade hook for keys that older versions auto-persisted from
|
|
86
|
+
// their then-current default. The settings modal in those versions wrote
|
|
87
|
+
// back the full payload on save -- including fields the user never
|
|
88
|
+
// touched -- so the next default change can't reach existing installs.
|
|
89
|
+
// Today's case: `codexTranscriptionModel: 'gpt-4o-transcribe'` was the
|
|
90
|
+
// legacy default before gpt-4o-transcribe-diarize shipped; clearing it
|
|
91
|
+
// here lets `getCodexTranscriptionModel()` return the current default
|
|
92
|
+
// (diarize) without forcing every user to manually unset it.
|
|
93
|
+
//
|
|
94
|
+
// The marker semantics are "we've considered migrating this user" --
|
|
95
|
+
// it lands on EVERY install on first launch, not just the ones we
|
|
96
|
+
// actually had to migrate. That way if a user later opts back into
|
|
97
|
+
// `gpt-4o-transcribe` deliberately (e.g. for glossary support), the
|
|
98
|
+
// next ConfigService construction sees the marker and skips the
|
|
99
|
+
// migration entirely instead of clobbering their explicit choice.
|
|
100
|
+
migrateLegacyDefaults() {
|
|
101
|
+
if (this.config.codexTranscriptionMigratedToDiarize)
|
|
102
|
+
return;
|
|
103
|
+
if (this.config.codexTranscriptionModel === 'gpt-4o-transcribe') {
|
|
104
|
+
this.setKey('codexTranscriptionModel', undefined);
|
|
105
|
+
}
|
|
106
|
+
this.setKey('codexTranscriptionMigratedToDiarize', true);
|
|
107
|
+
this.saveConfig();
|
|
75
108
|
}
|
|
76
109
|
loadConfig() {
|
|
77
110
|
try {
|
|
@@ -85,10 +118,53 @@ class ConfigService {
|
|
|
85
118
|
this.config = {};
|
|
86
119
|
}
|
|
87
120
|
}
|
|
121
|
+
setKey(key, value) {
|
|
122
|
+
if (value === undefined) {
|
|
123
|
+
delete this.config[key];
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.config[key] = value;
|
|
127
|
+
}
|
|
128
|
+
this.dirtyKeys.add(key);
|
|
129
|
+
}
|
|
88
130
|
saveConfig() {
|
|
89
131
|
try {
|
|
90
132
|
fs.mkdirSync(path.dirname(this.configPath), { recursive: true });
|
|
91
|
-
|
|
133
|
+
let merged = {};
|
|
134
|
+
if (fs.existsSync(this.configPath)) {
|
|
135
|
+
try {
|
|
136
|
+
merged = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// ignore corrupt disk file; treat as empty and let our writes recover it
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const key of this.dirtyKeys) {
|
|
143
|
+
const value = this.config[key];
|
|
144
|
+
if (value === undefined) {
|
|
145
|
+
delete merged[key];
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
merged[key] = value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// 0o600 keeps API keys + OAuth refresh tokens off other users on shared
|
|
152
|
+
// machines. writeFileSync's `mode` option only applies when the OS
|
|
153
|
+
// creates the file -- existing config.json from prior versions keeps its
|
|
154
|
+
// umask-derived mode (typically 0o644). Explicitly chmod after writing
|
|
155
|
+
// so upgrade paths get tightened too. chmodSync is a no-op for the bits
|
|
156
|
+
// that matter on Windows but doesn't throw, so the call is unconditional.
|
|
157
|
+
fs.writeFileSync(this.configPath, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
158
|
+
try {
|
|
159
|
+
fs.chmodSync(this.configPath, 0o600);
|
|
160
|
+
}
|
|
161
|
+
catch (chmodError) {
|
|
162
|
+
// Don't fail the save if chmod fails (e.g. exotic filesystem) -- the
|
|
163
|
+
// write succeeded and the override above already covers fresh files.
|
|
164
|
+
console.warn('Could not chmod config.json to 0o600:', chmodError);
|
|
165
|
+
}
|
|
166
|
+
this.config = merged;
|
|
167
|
+
this.dirtyKeys.clear();
|
|
92
168
|
}
|
|
93
169
|
catch (error) {
|
|
94
170
|
console.error('Error saving config:', error);
|
|
@@ -98,30 +174,82 @@ class ConfigService {
|
|
|
98
174
|
return this.config.geminiApiKey || process.env.GEMINI_API_KEY;
|
|
99
175
|
}
|
|
100
176
|
setGeminiApiKey(apiKey) {
|
|
101
|
-
this.
|
|
177
|
+
this.setKey('geminiApiKey', apiKey);
|
|
102
178
|
this.saveConfig();
|
|
103
179
|
}
|
|
180
|
+
getAiProvider() {
|
|
181
|
+
const envProvider = (0, aiProvider_1.normalizeAiProvider)(process.env.LISTENER_AI_PROVIDER);
|
|
182
|
+
if (envProvider) {
|
|
183
|
+
const configured = (0, aiProvider_1.normalizeAiProvider)(this.config.aiProvider);
|
|
184
|
+
if (configured && configured !== envProvider && !this.envProviderWarned) {
|
|
185
|
+
console.warn(`LISTENER_AI_PROVIDER=${envProvider} overrides configured aiProvider=${configured}.`);
|
|
186
|
+
this.envProviderWarned = true;
|
|
187
|
+
}
|
|
188
|
+
return envProvider;
|
|
189
|
+
}
|
|
190
|
+
const configured = (0, aiProvider_1.normalizeAiProvider)(this.config.aiProvider);
|
|
191
|
+
if (configured)
|
|
192
|
+
return configured;
|
|
193
|
+
if (!this.getGeminiApiKey() && this.hasCodexOAuth())
|
|
194
|
+
return 'codex';
|
|
195
|
+
return 'gemini';
|
|
196
|
+
}
|
|
197
|
+
setAiProvider(provider) {
|
|
198
|
+
this.setKey('aiProvider', provider);
|
|
199
|
+
this.saveConfig();
|
|
200
|
+
}
|
|
201
|
+
// Returns the active OAuth credentials whether they came from config or env.
|
|
202
|
+
// Callers that intend to PERSIST refreshed credentials must additionally check
|
|
203
|
+
// `hasStoredCodexOAuth()` and skip the persistence callback when source is env --
|
|
204
|
+
// otherwise a normal token refresh writes env-sourced tokens to plaintext disk.
|
|
205
|
+
getCodexOAuth() {
|
|
206
|
+
return this.config.codexOAuth || (0, codexOAuth_1.getCodexOAuthEnvCredentials)();
|
|
207
|
+
}
|
|
208
|
+
// True only when credentials are stored in config.json. Env-only credentials
|
|
209
|
+
// return false here. Use this to gate `onCodexOAuthUpdate` persistence callbacks.
|
|
210
|
+
hasStoredCodexOAuth() {
|
|
211
|
+
const c = this.config.codexOAuth;
|
|
212
|
+
return !!(c?.access && c.refresh && Number.isFinite(c.expires));
|
|
213
|
+
}
|
|
214
|
+
setCodexOAuth(credentials) {
|
|
215
|
+
this.setKey('codexOAuth', credentials);
|
|
216
|
+
this.saveConfig();
|
|
217
|
+
}
|
|
218
|
+
clearCodexOAuth() {
|
|
219
|
+
this.setKey('codexOAuth', undefined);
|
|
220
|
+
this.saveConfig();
|
|
221
|
+
}
|
|
222
|
+
hasCodexOAuth() {
|
|
223
|
+
return (0, codexOAuth_1.hasCodexOAuthEnvCredentials)() || this.hasStoredCodexOAuth();
|
|
224
|
+
}
|
|
225
|
+
hasAiAuth() {
|
|
226
|
+
const provider = this.getAiProvider();
|
|
227
|
+
if (provider === 'codex')
|
|
228
|
+
return this.hasCodexOAuth();
|
|
229
|
+
return !!this.getGeminiApiKey();
|
|
230
|
+
}
|
|
104
231
|
getNotionApiKey() {
|
|
105
232
|
return this.config.notionApiKey || process.env.NOTION_API_KEY;
|
|
106
233
|
}
|
|
107
234
|
setNotionApiKey(apiKey) {
|
|
108
|
-
this.
|
|
235
|
+
this.setKey('notionApiKey', apiKey);
|
|
109
236
|
this.saveConfig();
|
|
110
237
|
}
|
|
111
238
|
getNotionDatabaseId() {
|
|
112
239
|
return this.config.notionDatabaseId || process.env.NOTION_DATABASE_ID;
|
|
113
240
|
}
|
|
114
241
|
setNotionDatabaseId(databaseId) {
|
|
115
|
-
this.
|
|
242
|
+
this.setKey('notionDatabaseId', databaseId);
|
|
116
243
|
this.saveConfig();
|
|
117
244
|
}
|
|
118
245
|
hasRequiredConfig() {
|
|
119
|
-
return
|
|
246
|
+
return this.hasAiAuth() && !!this.getNotionApiKey() && !!this.getNotionDatabaseId();
|
|
120
247
|
}
|
|
121
248
|
getMissingConfigs() {
|
|
122
249
|
const missing = [];
|
|
123
|
-
if (!this.
|
|
124
|
-
missing.push('Gemini API Key');
|
|
250
|
+
if (!this.hasAiAuth()) {
|
|
251
|
+
missing.push(this.getAiProvider() === 'codex' ? 'Codex OAuth sign-in' : 'Gemini API Key');
|
|
252
|
+
}
|
|
125
253
|
if (!this.getNotionApiKey())
|
|
126
254
|
missing.push('Notion Integration Token');
|
|
127
255
|
if (!this.getNotionDatabaseId())
|
|
@@ -132,7 +260,7 @@ class ConfigService {
|
|
|
132
260
|
return this.config.autoMode || false;
|
|
133
261
|
}
|
|
134
262
|
setAutoMode(enabled) {
|
|
135
|
-
this.
|
|
263
|
+
this.setKey('autoMode', enabled);
|
|
136
264
|
this.saveConfig();
|
|
137
265
|
}
|
|
138
266
|
getMeetingDetection() {
|
|
@@ -142,63 +270,77 @@ class ConfigService {
|
|
|
142
270
|
return this.config.displayDetection || false;
|
|
143
271
|
}
|
|
144
272
|
setDisplayDetection(enabled) {
|
|
145
|
-
this.
|
|
273
|
+
this.setKey('displayDetection', enabled);
|
|
146
274
|
this.saveConfig();
|
|
147
275
|
}
|
|
148
276
|
getGlobalShortcut() {
|
|
149
277
|
return this.config.globalShortcut || 'CommandOrControl+Shift+L';
|
|
150
278
|
}
|
|
151
279
|
setGlobalShortcut(shortcut) {
|
|
152
|
-
this.
|
|
280
|
+
this.setKey('globalShortcut', shortcut);
|
|
153
281
|
this.saveConfig();
|
|
154
282
|
}
|
|
155
283
|
getKnownWords() {
|
|
156
284
|
return this.config.knownWords || [];
|
|
157
285
|
}
|
|
158
286
|
setKnownWords(words) {
|
|
159
|
-
this.
|
|
287
|
+
this.setKey('knownWords', words);
|
|
160
288
|
this.saveConfig();
|
|
161
289
|
}
|
|
162
290
|
getGeminiModel() {
|
|
163
|
-
return this.config.geminiModel ||
|
|
291
|
+
return this.config.geminiModel || aiProvider_1.DEFAULT_GEMINI_MODEL;
|
|
164
292
|
}
|
|
165
293
|
setGeminiModel(model) {
|
|
166
|
-
this.
|
|
294
|
+
this.setKey('geminiModel', model);
|
|
167
295
|
this.saveConfig();
|
|
168
296
|
}
|
|
169
297
|
getGeminiFlashModel() {
|
|
170
|
-
return this.config.geminiFlashModel ||
|
|
298
|
+
return this.config.geminiFlashModel || aiProvider_1.DEFAULT_GEMINI_FLASH_MODEL;
|
|
171
299
|
}
|
|
172
300
|
setGeminiFlashModel(model) {
|
|
173
|
-
this.
|
|
301
|
+
this.setKey('geminiFlashModel', model);
|
|
302
|
+
this.saveConfig();
|
|
303
|
+
}
|
|
304
|
+
getCodexModel() {
|
|
305
|
+
return this.config.codexModel || aiProvider_1.DEFAULT_CODEX_MODEL;
|
|
306
|
+
}
|
|
307
|
+
setCodexModel(model) {
|
|
308
|
+
this.setKey('codexModel', model);
|
|
309
|
+
this.saveConfig();
|
|
310
|
+
}
|
|
311
|
+
getCodexTranscriptionModel() {
|
|
312
|
+
return this.config.codexTranscriptionModel || aiProvider_1.DEFAULT_CODEX_TRANSCRIPTION_MODEL;
|
|
313
|
+
}
|
|
314
|
+
setCodexTranscriptionModel(model) {
|
|
315
|
+
this.setKey('codexTranscriptionModel', model);
|
|
174
316
|
this.saveConfig();
|
|
175
317
|
}
|
|
176
318
|
getMaxRecordingMinutes() {
|
|
177
319
|
return this.config.maxRecordingMinutes || 0;
|
|
178
320
|
}
|
|
179
321
|
setMaxRecordingMinutes(minutes) {
|
|
180
|
-
this.
|
|
322
|
+
this.setKey('maxRecordingMinutes', Math.max(0, Math.floor(minutes)));
|
|
181
323
|
this.saveConfig();
|
|
182
324
|
}
|
|
183
325
|
getRecordingReminderMinutes() {
|
|
184
326
|
return this.config.recordingReminderMinutes || 0;
|
|
185
327
|
}
|
|
186
328
|
setRecordingReminderMinutes(minutes) {
|
|
187
|
-
this.
|
|
329
|
+
this.setKey('recordingReminderMinutes', Math.max(0, Math.floor(minutes)));
|
|
188
330
|
this.saveConfig();
|
|
189
331
|
}
|
|
190
332
|
getMinRecordingSeconds() {
|
|
191
333
|
return this.config.minRecordingSeconds || 0;
|
|
192
334
|
}
|
|
193
335
|
setMinRecordingSeconds(seconds) {
|
|
194
|
-
this.
|
|
336
|
+
this.setKey('minRecordingSeconds', Math.max(0, Math.floor(seconds)));
|
|
195
337
|
this.saveConfig();
|
|
196
338
|
}
|
|
197
339
|
getRecordSystemAudio() {
|
|
198
340
|
return this.config.recordSystemAudio || false;
|
|
199
341
|
}
|
|
200
342
|
setRecordSystemAudio(enabled) {
|
|
201
|
-
this.
|
|
343
|
+
this.setKey('recordSystemAudio', enabled);
|
|
202
344
|
this.saveConfig();
|
|
203
345
|
}
|
|
204
346
|
getAudioDeviceId() {
|
|
@@ -208,47 +350,51 @@ class ConfigService {
|
|
|
208
350
|
return this.config.lastSeenVersion;
|
|
209
351
|
}
|
|
210
352
|
setLastSeenVersion(version) {
|
|
211
|
-
this.
|
|
353
|
+
this.setKey('lastSeenVersion', version);
|
|
212
354
|
this.saveConfig();
|
|
213
355
|
}
|
|
214
356
|
getSummaryPrompt() {
|
|
215
357
|
return this.config.summaryPrompt || exports.DEFAULT_SUMMARY_PROMPT;
|
|
216
358
|
}
|
|
217
359
|
setSummaryPrompt(prompt) {
|
|
218
|
-
this.
|
|
360
|
+
this.setKey('summaryPrompt', prompt);
|
|
219
361
|
this.saveConfig();
|
|
220
362
|
}
|
|
221
363
|
getSlackWebhookUrl() {
|
|
222
364
|
return this.config.slackWebhookUrl || process.env.SLACK_WEBHOOK_URL;
|
|
223
365
|
}
|
|
224
366
|
setSlackWebhookUrl(url) {
|
|
225
|
-
this.
|
|
367
|
+
this.setKey('slackWebhookUrl', url);
|
|
226
368
|
this.saveConfig();
|
|
227
369
|
}
|
|
228
370
|
getSlackAutoShare() {
|
|
229
371
|
return this.config.slackAutoShare || false;
|
|
230
372
|
}
|
|
231
373
|
setSlackAutoShare(enabled) {
|
|
232
|
-
this.
|
|
374
|
+
this.setKey('slackAutoShare', enabled);
|
|
233
375
|
this.saveConfig();
|
|
234
376
|
}
|
|
235
377
|
updateConfig(partial) {
|
|
236
378
|
for (const [key, value] of Object.entries(partial)) {
|
|
237
379
|
if (value !== undefined) {
|
|
238
|
-
this.
|
|
380
|
+
this.setKey(key, value);
|
|
239
381
|
}
|
|
240
382
|
}
|
|
241
383
|
this.saveConfig();
|
|
242
384
|
}
|
|
243
385
|
unsetKey(key) {
|
|
244
|
-
|
|
386
|
+
this.setKey(key, undefined);
|
|
245
387
|
this.saveConfig();
|
|
246
388
|
}
|
|
247
389
|
getAllConfig() {
|
|
248
390
|
return {
|
|
391
|
+
aiProvider: this.getAiProvider(),
|
|
249
392
|
geminiApiKey: this.getGeminiApiKey(),
|
|
250
393
|
geminiModel: this.getGeminiModel(),
|
|
251
394
|
geminiFlashModel: this.getGeminiFlashModel(),
|
|
395
|
+
codexModel: this.getCodexModel(),
|
|
396
|
+
codexTranscriptionModel: this.getCodexTranscriptionModel(),
|
|
397
|
+
codexOAuthConfigured: this.hasCodexOAuth(),
|
|
252
398
|
notionApiKey: this.getNotionApiKey(),
|
|
253
399
|
notionDatabaseId: this.getNotionDatabaseId(),
|
|
254
400
|
autoMode: this.getAutoMode(),
|
package/dist/dataPath.js
CHANGED
|
@@ -34,10 +34,27 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getDataPath = getDataPath;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
37
38
|
const os = __importStar(require("os"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
|
-
|
|
40
|
-
const
|
|
40
|
+
const APP_NAME = 'listener-ai';
|
|
41
|
+
const LEGACY_APP_NAME = 'Listener.AI';
|
|
42
|
+
function platformDataPath(appName) {
|
|
43
|
+
switch (process.platform) {
|
|
44
|
+
case 'darwin':
|
|
45
|
+
return path.join(os.homedir(), 'Library', 'Application Support', appName);
|
|
46
|
+
case 'win32':
|
|
47
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), appName);
|
|
48
|
+
default:
|
|
49
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), appName);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function hasUserData(dataPath) {
|
|
53
|
+
return (fs.existsSync(path.join(dataPath, 'config.json')) ||
|
|
54
|
+
fs.existsSync(path.join(dataPath, 'transcriptions')) ||
|
|
55
|
+
fs.existsSync(path.join(dataPath, 'recordings')) ||
|
|
56
|
+
fs.existsSync(path.join(dataPath, 'metadata')));
|
|
57
|
+
}
|
|
41
58
|
function getDataPath() {
|
|
42
59
|
// Test escape hatch: integration tests set this to a temp dir to avoid
|
|
43
60
|
// touching the user's real data. Gated on NODE_ENV=test so a stray
|
|
@@ -46,12 +63,15 @@ function getDataPath() {
|
|
|
46
63
|
if (process.env.LISTENER_DATA_PATH && process.env.NODE_ENV === 'test') {
|
|
47
64
|
return process.env.LISTENER_DATA_PATH;
|
|
48
65
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
const currentPath = platformDataPath(APP_NAME);
|
|
67
|
+
const legacyPath = platformDataPath(LEGACY_APP_NAME);
|
|
68
|
+
if (hasUserData(currentPath))
|
|
69
|
+
return currentPath;
|
|
70
|
+
if (hasUserData(legacyPath))
|
|
71
|
+
return legacyPath;
|
|
72
|
+
if (fs.existsSync(currentPath))
|
|
73
|
+
return currentPath;
|
|
74
|
+
if (fs.existsSync(legacyPath))
|
|
75
|
+
return legacyPath;
|
|
76
|
+
return currentPath;
|
|
57
77
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.importEsm = void 0;
|
|
4
|
+
// Dynamic ESM import that survives tsc's CJS rewrite. With
|
|
5
|
+
// `module: "commonjs"` in tsconfig.json, a plain `import('pkg')` compiles to
|
|
6
|
+
// `Promise.resolve().then(() => require('pkg'))`, which fails on ESM-only
|
|
7
|
+
// packages with ERR_REQUIRE_ESM. `Function(...)` evaluates at runtime, so the
|
|
8
|
+
// literal `import()` survives untouched and stays an actual dynamic import.
|
|
9
|
+
//
|
|
10
|
+
// Use this for any ESM-only dependency (`@earendil-works/pi-ai`,
|
|
11
|
+
// `@earendil-works/pi-ai/oauth`).
|
|
12
|
+
exports.importEsm = (() => {
|
|
13
|
+
const fn = new Function('specifier', 'return import(specifier)');
|
|
14
|
+
return fn;
|
|
15
|
+
})();
|