NotebookLM CLI 로그인 버튼 — nlm 실행 파일 자동 탐색

- 사이드바 도구 메뉴에 'NotebookLM 로그인' 버튼 추가, 파이썬 Scripts 폴더에서
  nlm(.exe) 자동 탐색(버전 하드코딩 없음) 후 login 인자로 실행
- g1nation.notebookLmCliPath 설정으로 수동 경로 지정도 가능
This commit is contained in:
2026-08-06 18:51:22 +09:00
parent 769cf01a99
commit b61e06ef41
13 changed files with 173 additions and 21 deletions
+111
View File
@@ -23,6 +23,7 @@ import { execShell } from '../../lib/execUtil';
*/
const TERMINAL_NAME = 'NotebookLM Bridge';
const LOGIN_TERMINAL_NAME = 'NotebookLM Login';
const BRIDGE_PORT = 3002;
/** 자동 탐색 시 찾는 프로젝트 폴더 이름 후보 (맥/윈도우 클론 이름이 다를 수 있음). */
const PROJECT_DIR_NAMES = ['Datacollector_MAC', 'Datacollector', 'Datacollect'];
@@ -55,6 +56,71 @@ export function buildPortKillCommand(platform: NodeJS.Platform, port: number): s
return `lsof -ti :${port} | xargs kill 2>/dev/null`;
}
/**
* `nlm` 실행 파일 후보 경로를 우선순위대로 나열한다 (존재 검증 안 함 — 순수 함수).
*
* nlm 은 pip 로 설치되는 콘솔 스크립트라 PATH 에 없는 경우가 흔하다(실측: 이 PC 도 PATH 에
* 없고 %APPDATA%\Python\Python314\Scripts 에만 있었다). 파이썬 버전이 올라가면 경로가
* 바뀌므로 특정 버전을 하드코딩하지 않고 Scripts 디렉터리들을 훑는다.
* 마지막 후보로 맨 이름('nlm')을 두어 PATH 에 있는 환경에서는 그대로 동작하게 한다.
*/
export function candidateNlmPaths(
platform: NodeJS.Platform,
env: Record<string, string | undefined>,
pythonScriptDirs: string[] = [],
cfgPath = '',
): string[] {
const out: string[] = [];
const push = (p: string) => { if (p && !out.includes(p)) out.push(p); };
if (cfgPath && cfgPath.trim()) push(cfgPath.trim());
if (platform === 'win32') {
for (const d of pythonScriptDirs) push(path.join(d, 'nlm.exe'));
const local = env.LOCALAPPDATA;
if (local) push(path.join(local, 'Programs', 'Python', 'Scripts', 'nlm.exe'));
push('nlm.exe');
} else {
for (const d of pythonScriptDirs) push(path.join(d, 'nlm'));
const home = env.HOME;
if (home) {
push(path.join(home, '.local', 'bin', 'nlm'));
push(path.join(home, 'Library', 'Python', 'bin', 'nlm'));
}
push('/usr/local/bin/nlm');
}
push('nlm'); // PATH 폴백
return out;
}
/** 파이썬 사용자 설치 경로 아래의 Scripts(win) / bin(mac) 디렉터리들을 나열한다. */
function pythonScriptDirs(): string[] {
const dirs: string[] = [];
const roots = process.platform === 'win32'
? [process.env.APPDATA ? path.join(process.env.APPDATA, 'Python') : '']
: [process.env.HOME ? path.join(process.env.HOME, 'Library', 'Python') : ''];
for (const root of roots.filter(Boolean)) {
try {
for (const name of fs.readdirSync(root)) {
const s = path.join(root, name, 'Scripts');
const b = path.join(root, name, 'bin');
if (fs.existsSync(s)) dirs.push(s);
if (fs.existsSync(b)) dirs.push(b);
}
} catch { /* 접근 불가 — skip */ }
}
return dirs;
}
/** 실제 존재하는 nlm 실행 파일 경로. 못 찾으면 null (PATH 폴백은 호출측이 판단). */
function findNlmExecutable(): string | null {
const cfg = vscode.workspace.getConfiguration('g1nation').get<string>('notebookLmCliPath', '') || '';
for (const cand of candidateNlmPaths(process.platform, process.env, pythonScriptDirs(), cfg)) {
if (!path.isAbsolute(cand)) continue; // PATH 이름은 존재 검증 대상이 아님
try { if (fs.existsSync(cand)) return cand; } catch { /* skip */ }
}
return null;
}
function findBridgeProject(): string | null {
const cfgPath = vscode.workspace.getConfiguration('g1nation').get<string>('datacollectLocalProjectPath', '') || '';
const wsDirs = (vscode.workspace.workspaceFolders || [])
@@ -141,6 +207,48 @@ export async function stopNotebookLmBridge(): Promise<void> {
}
}
/**
* NotebookLM 재로그인(구글 인증) 원클릭.
*
* 배경: NotebookLM 토큰은 ~90분에 만료되고, 전용 프로필의 구글 **세션 자체**가 끊기면
* 자동 갱신(Datacollect 의 스케줄 작업)이 매번 실패하면서 로그인 창만 반복해서 띄운다.
* 복구는 `nlm login` 을 한 번 사람이 실행해 브라우저에서 인증하는 것뿐인데, 그러려면
* 터미널과 실행 파일 경로를 알아야 했다 — 그 한 단계를 버튼으로 없앤다.
*
* 구현: 셸을 거치지 않고 실행 파일을 터미널 프로세스로 직접 띄운다(shellPath/shellArgs).
* 경로에 공백이 있어도 따옴표 처리가 필요 없고, PowerShell/cmd/zsh 차이도 사라진다.
* 인증 결과(성공/실패)는 터미널에 그대로 보이므로 별도 폴링을 하지 않는다.
*/
export async function loginNotebookLm(): Promise<void> {
let exe = findNlmExecutable();
if (!exe) {
const pick = await vscode.window.showErrorMessage(
'NotebookLM CLI(nlm) 실행 파일을 찾지 못했습니다. 파일을 직접 선택하면 설정에 저장되어 다음부터는 버튼만 누르면 됩니다.',
'파일 선택'
);
if (pick !== '파일 선택') return;
const chosen = await vscode.window.showOpenDialog({
canSelectFiles: true, canSelectFolders: false, canSelectMany: false,
openLabel: 'nlm 실행 파일 선택',
});
const f = chosen?.[0]?.fsPath;
if (!f) return;
await vscode.workspace.getConfiguration('g1nation')
.update('notebookLmCliPath', f, vscode.ConfigurationTarget.Global);
exe = f;
}
vscode.window.terminals.find(t => t.name === LOGIN_TERMINAL_NAME)?.dispose();
const terminal = vscode.window.createTerminal({
name: LOGIN_TERMINAL_NAME, shellPath: exe, shellArgs: ['login'],
});
terminal.show(true);
vscode.window.showInformationMessage(
'NotebookLM 로그인을 시작했습니다 — 브라우저에서 구글 인증을 완료하세요. ' +
'결과는 "NotebookLM Login" 터미널에 표시됩니다.'
);
}
/** 사이드바 웹뷰 메시지 핸들러 — sidebarProvider 의 핸들러 체인에 등록된다. */
export async function handleBridgeLauncherMessage(_provider: unknown, data: any): Promise<boolean> {
switch (data?.type) {
@@ -150,6 +258,9 @@ export async function handleBridgeLauncherMessage(_provider: unknown, data: any)
case 'stopNotebookLmBridge':
await stopNotebookLmBridge();
return true;
case 'loginNotebookLm':
await loginNotebookLm();
return true;
default:
return false;
}