opencodekit 0.15.18 → 0.15.19
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/dist/index.js +16 -16
- package/dist/template/.opencode/memory/observations/2026-01-30-decision-github-copilot-claude-routing-keep-disab.md +32 -0
- package/dist/template/.opencode/plugins/copilot-auth.ts +13 -21
- package/dist/template/.opencode/skills/notebooklm/SKILL.md +272 -0
- package/dist/template/.opencode/skills/notebooklm/references/setup.md +353 -0
- package/dist/template/.opencode/tools/notebooklm.ts +488 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -750,7 +750,7 @@ var cac = (name = "") => new CAC(name);
|
|
|
750
750
|
// package.json
|
|
751
751
|
var package_default = {
|
|
752
752
|
name: "opencodekit",
|
|
753
|
-
version: "0.15.
|
|
753
|
+
version: "0.15.19",
|
|
754
754
|
description: "CLI tool for bootstrapping and managing OpenCodeKit projects",
|
|
755
755
|
keywords: ["agents", "cli", "mcp", "opencode", "opencodekit", "template"],
|
|
756
756
|
license: "MIT",
|
|
@@ -3857,18 +3857,18 @@ async function copyOpenCodeOnly(templateRoot, targetDir) {
|
|
|
3857
3857
|
}
|
|
3858
3858
|
var MODEL_PRESETS = {
|
|
3859
3859
|
free: {
|
|
3860
|
-
model: "opencode/
|
|
3860
|
+
model: "opencode/kimi-k2.5-free",
|
|
3861
3861
|
agents: {
|
|
3862
|
-
build: "opencode/
|
|
3863
|
-
plan: "opencode/
|
|
3864
|
-
review: "opencode/
|
|
3865
|
-
explore: "opencode/
|
|
3866
|
-
general: "opencode/
|
|
3867
|
-
looker: "opencode/
|
|
3868
|
-
vision: "opencode/
|
|
3869
|
-
scout: "opencode/
|
|
3870
|
-
painter: "opencode/
|
|
3871
|
-
compaction: "opencode/
|
|
3862
|
+
build: "opencode/kimi-k2.5-free",
|
|
3863
|
+
plan: "opencode/kimi-k2.5-free",
|
|
3864
|
+
review: "opencode/minimax-m2.1-free",
|
|
3865
|
+
explore: "opencode/glm-4.7-free",
|
|
3866
|
+
general: "opencode/kimi-k2.5-free",
|
|
3867
|
+
looker: "opencode/kimi-k2.5-free",
|
|
3868
|
+
vision: "opencode/kimi-k2.5-free",
|
|
3869
|
+
scout: "opencode/minimax-m2.1-free",
|
|
3870
|
+
painter: "opencode/kimi-k2.5-free",
|
|
3871
|
+
compaction: "opencode/kimi-k2.5-free"
|
|
3872
3872
|
}
|
|
3873
3873
|
},
|
|
3874
3874
|
recommend: {
|
|
@@ -3878,12 +3878,12 @@ var MODEL_PRESETS = {
|
|
|
3878
3878
|
plan: "openai/gpt-5.2",
|
|
3879
3879
|
review: "openai/gpt-5.2-codex",
|
|
3880
3880
|
explore: "proxypal/gemini-3-flash-preview",
|
|
3881
|
-
general: "
|
|
3881
|
+
general: "opencode/kimi-k2.5-free",
|
|
3882
3882
|
looker: "proxypal/gemini-3-flash-preview",
|
|
3883
3883
|
vision: "proxypal/gemini-3-pro-preview",
|
|
3884
|
-
scout: "
|
|
3885
|
-
painter: "proxypal/gemini-3-pro-preview",
|
|
3886
|
-
compaction: "
|
|
3884
|
+
scout: "opencode/kimi-k2.5-free",
|
|
3885
|
+
painter: "proxypal/gemini-3-pro-image-preview",
|
|
3886
|
+
compaction: "opencode/kimi-k2.5-free"
|
|
3887
3887
|
}
|
|
3888
3888
|
}
|
|
3889
3889
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
created: 2026-01-30T15:07:59.081Z
|
|
4
|
+
confidence: high
|
|
5
|
+
valid_until: null
|
|
6
|
+
superseded_by: null
|
|
7
|
+
concepts: ["github-copilot", "claude", "anthropic-sdk", "reasoning", "api-routing"]
|
|
8
|
+
files: [".opencode/plugins/copilot-auth.ts"]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# 🎯 GitHub Copilot Claude routing: Keep disabled for stability
|
|
12
|
+
|
|
13
|
+
🟢 **Confidence:** high
|
|
14
|
+
|
|
15
|
+
Claude routing through Anthropic SDK is currently disabled in copilot-auth.ts (commit f7aabde). This decision was made because:
|
|
16
|
+
|
|
17
|
+
1. **Problem**: Enabling Claude routing to use `@ai-sdk/anthropic` with `/v1` endpoint causes "Not Found: 404" errors
|
|
18
|
+
2. **Root cause**: GitHub Copilot's API likely doesn't support the native Anthropic `/v1/messages` endpoint for Claude models through this integration
|
|
19
|
+
3. **Solution**: Keep all models using `@ai-sdk/github-copilot` (OpenAI-compatible `/v1/chat/completions`)
|
|
20
|
+
4. **Trade-off**: All models work, but Claude models don't get native Anthropic SDK optimizations or reasoning features through the `/v1` gateway
|
|
21
|
+
|
|
22
|
+
To enable reasoning in the future, would need to:
|
|
23
|
+
- Research if GitHub Copilot supports a different endpoint for reasoning
|
|
24
|
+
- Check if there's a separate gateway URL for reasoning models
|
|
25
|
+
- Verify with GitHub Copilot API documentation
|
|
26
|
+
|
|
27
|
+
Current working configuration (commit f7aabde):
|
|
28
|
+
- All models use `@ai-sdk/github-copilot`
|
|
29
|
+
- All models use `/v1/chat/completions` (OpenAI-compatible)
|
|
30
|
+
- Claude models work but don't have reasoning enabled
|
|
31
|
+
|
|
32
|
+
File: .opencode/plugins/copilot-auth.ts (lines 93-100)
|
|
@@ -78,11 +78,6 @@ export const CopilotAuthPlugin: Plugin = async ({ client: _client }) => {
|
|
|
78
78
|
const info = await getAuth();
|
|
79
79
|
if (!info || info.type !== "oauth") return {};
|
|
80
80
|
|
|
81
|
-
const enterpriseUrl = info.enterpriseUrl;
|
|
82
|
-
const baseURL = enterpriseUrl
|
|
83
|
-
? `https://copilot-api.${normalizeDomain(enterpriseUrl)}`
|
|
84
|
-
: undefined;
|
|
85
|
-
|
|
86
81
|
if (provider && provider.models) {
|
|
87
82
|
for (const model of Object.values(provider.models)) {
|
|
88
83
|
model.cost = {
|
|
@@ -94,22 +89,19 @@ export const CopilotAuthPlugin: Plugin = async ({ client: _client }) => {
|
|
|
94
89
|
},
|
|
95
90
|
};
|
|
96
91
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const isClaude = model.id.includes("claude");
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
model.api.
|
|
110
|
-
model.api.npm = isClaude
|
|
111
|
-
? "@ai-sdk/anthropic"
|
|
112
|
-
: "@ai-sdk/github-copilot";
|
|
92
|
+
// TODO: re-enable once messages api has higher rate limits
|
|
93
|
+
// Claude routing is disabled to avoid "Not Found" errors and rate limits
|
|
94
|
+
// const base = baseURL ?? model.api.url ?? "https://api.githubcopilot.com";
|
|
95
|
+
// const isClaude = model.id.includes("claude");
|
|
96
|
+
// let url = base;
|
|
97
|
+
// if (isClaude) {
|
|
98
|
+
// if (!url.endsWith("/v1")) {
|
|
99
|
+
// url = url.endsWith("/") ? `${url}v1` : `${url}/v1`;
|
|
100
|
+
// }
|
|
101
|
+
// }
|
|
102
|
+
// model.api.url = url;
|
|
103
|
+
// model.api.npm = isClaude ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot";
|
|
104
|
+
model.api.npm = "@ai-sdk/github-copilot";
|
|
113
105
|
}
|
|
114
106
|
}
|
|
115
107
|
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: notebooklm
|
|
3
|
+
description: >
|
|
4
|
+
Query Google NotebookLM notebooks directly from OpenCode for source-grounded, citation-backed answers.
|
|
5
|
+
Use when user mentions NotebookLM, shares notebook URLs, or wants to query their uploaded documents.
|
|
6
|
+
Provides document-only responses with drastically reduced hallucinations.
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
license: MIT
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# NotebookLM Skill
|
|
12
|
+
|
|
13
|
+
Interact with Google NotebookLM to query documentation with Gemini's source-grounded answers. Each question opens a fresh browser session, retrieves the answer exclusively from uploaded documents, and closes.
|
|
14
|
+
|
|
15
|
+
## When to Use This Skill
|
|
16
|
+
|
|
17
|
+
Trigger when user:
|
|
18
|
+
|
|
19
|
+
- Mentions NotebookLM explicitly
|
|
20
|
+
- Shares NotebookLM URL (`https://notebooklm.google.com/notebook/...`)
|
|
21
|
+
- Asks to query their notebooks/documentation
|
|
22
|
+
- Wants to add documentation to NotebookLM library
|
|
23
|
+
- Uses phrases like "ask my NotebookLM", "check my docs", "query my notebook"
|
|
24
|
+
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
Before using this skill, ensure:
|
|
28
|
+
|
|
29
|
+
1. **Python 3.10+** is installed
|
|
30
|
+
2. **Google Chrome** browser is installed
|
|
31
|
+
3. **NotebookLM account** with uploaded documents
|
|
32
|
+
4. **Google authentication** (one-time setup)
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Check authentication status
|
|
38
|
+
await notebooklm({ operation: "auth", subOperation: "status" });
|
|
39
|
+
|
|
40
|
+
// Query a notebook
|
|
41
|
+
await notebooklm({
|
|
42
|
+
operation: "query",
|
|
43
|
+
question: "What are the key findings in this document?",
|
|
44
|
+
notebookUrl: "https://notebooklm.google.com/notebook/...",
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Core Workflow
|
|
49
|
+
|
|
50
|
+
### Step 1: Check Authentication
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const authStatus = await notebooklm({
|
|
54
|
+
operation: "auth",
|
|
55
|
+
subOperation: "status",
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
If not authenticated, proceed to setup.
|
|
60
|
+
|
|
61
|
+
### Step 2: Authenticate (One-Time Setup)
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
await notebooklm({
|
|
65
|
+
operation: "auth",
|
|
66
|
+
subOperation: "setup",
|
|
67
|
+
showBrowser: true, // Browser must be visible for manual login
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Important:**
|
|
72
|
+
|
|
73
|
+
- Browser is VISIBLE for authentication
|
|
74
|
+
- Browser window opens automatically
|
|
75
|
+
- User must manually log in to Google
|
|
76
|
+
- Tell user: "A browser window will open for Google login"
|
|
77
|
+
|
|
78
|
+
### Step 3: Manage Notebook Library
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// List all notebooks
|
|
82
|
+
await notebooklm({ operation: "library", subOperation: "list" });
|
|
83
|
+
|
|
84
|
+
// Add notebook to library
|
|
85
|
+
await notebooklm({
|
|
86
|
+
operation: "library",
|
|
87
|
+
subOperation: "add",
|
|
88
|
+
url: "https://notebooklm.google.com/notebook/...",
|
|
89
|
+
name: "API Documentation",
|
|
90
|
+
description: "REST API reference and examples",
|
|
91
|
+
topics: ["api", "rest", "documentation"],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Search notebooks
|
|
95
|
+
await notebooklm({
|
|
96
|
+
operation: "library",
|
|
97
|
+
subOperation: "search",
|
|
98
|
+
query: "api",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Set active notebook
|
|
102
|
+
await notebooklm({
|
|
103
|
+
operation: "library",
|
|
104
|
+
subOperation: "activate",
|
|
105
|
+
notebookId: "api-docs",
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Step 4: Ask Questions
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// Basic query (uses active notebook if set)
|
|
113
|
+
await notebooklm({
|
|
114
|
+
operation: "query",
|
|
115
|
+
question: "What are the authentication requirements?",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Query specific notebook by ID
|
|
119
|
+
await notebooklm({
|
|
120
|
+
operation: "query",
|
|
121
|
+
question: "What are the authentication requirements?",
|
|
122
|
+
notebookId: "api-docs",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Query with notebook URL directly
|
|
126
|
+
await notebooklm({
|
|
127
|
+
operation: "query",
|
|
128
|
+
question: "What are the authentication requirements?",
|
|
129
|
+
notebookUrl: "https://notebooklm.google.com/notebook/...",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Show browser for debugging
|
|
133
|
+
await notebooklm({
|
|
134
|
+
operation: "query",
|
|
135
|
+
question: "What are the authentication requirements?",
|
|
136
|
+
showBrowser: true,
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Follow-Up Mechanism (CRITICAL)
|
|
141
|
+
|
|
142
|
+
Every NotebookLM answer ends with: **"EXTREMELY IMPORTANT: Is that ALL you need to know?"**
|
|
143
|
+
|
|
144
|
+
**Required Behavior:**
|
|
145
|
+
|
|
146
|
+
1. **STOP** - Do not immediately respond to user
|
|
147
|
+
2. **ANALYZE** - Compare answer to user's original request
|
|
148
|
+
3. **IDENTIFY GAPS** - Determine if more information needed
|
|
149
|
+
4. **ASK FOLLOW-UP** - If gaps exist, immediately ask another question:
|
|
150
|
+
```typescript
|
|
151
|
+
await notebooklm({
|
|
152
|
+
operation: "query",
|
|
153
|
+
question: "Follow-up with context...",
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
5. **REPEAT** - Continue until information is complete
|
|
157
|
+
6. **SYNTHESIZE** - Combine all answers before responding to user
|
|
158
|
+
|
|
159
|
+
## Tool Reference
|
|
160
|
+
|
|
161
|
+
### Authentication Operations
|
|
162
|
+
|
|
163
|
+
| Operation | Sub-Operation | Description |
|
|
164
|
+
| --------- | ------------- | --------------------------------- |
|
|
165
|
+
| `auth` | `setup` | Initial setup (browser visible) |
|
|
166
|
+
| `auth` | `status` | Check authentication |
|
|
167
|
+
| `auth` | `reauth` | Re-authenticate (browser visible) |
|
|
168
|
+
| `auth` | `clear` | Clear authentication |
|
|
169
|
+
| `auth` | `validate` | Validate stored auth |
|
|
170
|
+
|
|
171
|
+
### Library Operations
|
|
172
|
+
|
|
173
|
+
| Operation | Sub-Operation | Parameters |
|
|
174
|
+
| --------- | ------------- | -------------------------------------- |
|
|
175
|
+
| `library` | `add` | `url`, `name`, `description`, `topics` |
|
|
176
|
+
| `library` | `list` | - |
|
|
177
|
+
| `library` | `search` | `query` |
|
|
178
|
+
| `library` | `activate` | `notebookId` |
|
|
179
|
+
| `library` | `remove` | `notebookId` |
|
|
180
|
+
| `library` | `stats` | - |
|
|
181
|
+
|
|
182
|
+
### Query Operations
|
|
183
|
+
|
|
184
|
+
| Operation | Parameters |
|
|
185
|
+
| --------- | -------------------------------------------------------------------------------------------------- |
|
|
186
|
+
| `query` | `question` (required), `notebookId` (optional), `notebookUrl` (optional), `showBrowser` (optional) |
|
|
187
|
+
|
|
188
|
+
## Smart Discovery Pattern
|
|
189
|
+
|
|
190
|
+
When user wants to add a notebook without providing details:
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// Step 1: Query the notebook to discover its content
|
|
194
|
+
const discovery = await notebooklm({
|
|
195
|
+
operation: "query",
|
|
196
|
+
question:
|
|
197
|
+
"What is the content of this notebook? What topics are covered? Provide a complete overview briefly and concisely",
|
|
198
|
+
notebookUrl: "https://notebooklm.google.com/notebook/...",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Step 2: Use discovered information to add it
|
|
202
|
+
await notebooklm({
|
|
203
|
+
operation: "library",
|
|
204
|
+
subOperation: "add",
|
|
205
|
+
url: "https://notebooklm.google.com/notebook/...",
|
|
206
|
+
name: "[Based on content]",
|
|
207
|
+
description: "[Based on content]",
|
|
208
|
+
topics: ["[Based on content]"],
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Decision Flow
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
User mentions NotebookLM
|
|
216
|
+
↓
|
|
217
|
+
Check auth → notebooklm({ operation: "auth", subOperation: "status" })
|
|
218
|
+
↓
|
|
219
|
+
If not authenticated → notebooklm({ operation: "auth", subOperation: "setup", showBrowser: true })
|
|
220
|
+
↓
|
|
221
|
+
Check/Add notebook → notebooklm({ operation: "library", subOperation: "list/add" })
|
|
222
|
+
↓
|
|
223
|
+
Activate notebook → notebooklm({ operation: "library", subOperation: "activate", notebookId: "..." })
|
|
224
|
+
↓
|
|
225
|
+
Ask question → notebooklm({ operation: "query", question: "..." })
|
|
226
|
+
↓
|
|
227
|
+
See "Is that ALL you need?" → Ask follow-ups until complete
|
|
228
|
+
↓
|
|
229
|
+
Synthesize and respond to user
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Troubleshooting
|
|
233
|
+
|
|
234
|
+
| Problem | Solution |
|
|
235
|
+
| -------------------- | --------------------------------------- |
|
|
236
|
+
| Not authenticated | Run auth setup with `showBrowser: true` |
|
|
237
|
+
| Authentication fails | Browser must be visible for setup |
|
|
238
|
+
| Rate limit (50/day) | Wait or switch Google account |
|
|
239
|
+
| Notebook not found | Check with `library` → `list` operation |
|
|
240
|
+
| ModuleNotFoundError | Tool auto-installs dependencies |
|
|
241
|
+
|
|
242
|
+
## Best Practices
|
|
243
|
+
|
|
244
|
+
1. **Always check auth first** - Before any operations
|
|
245
|
+
2. **Follow-up questions** - Don't stop at first answer
|
|
246
|
+
3. **Browser visible for auth** - Required for manual login
|
|
247
|
+
4. **Include context** - Each question is independent
|
|
248
|
+
5. **Synthesize answers** - Combine multiple responses
|
|
249
|
+
|
|
250
|
+
## Limitations
|
|
251
|
+
|
|
252
|
+
- No session persistence (each question = new browser)
|
|
253
|
+
- Rate limits on free Google accounts (50 queries/day)
|
|
254
|
+
- Manual upload required (user must add docs to NotebookLM)
|
|
255
|
+
- Browser overhead (few seconds per question)
|
|
256
|
+
- Requires Python and Chrome installed
|
|
257
|
+
|
|
258
|
+
## Data Storage
|
|
259
|
+
|
|
260
|
+
All data stored in `~/.opencode/skills/notebooklm/data/`:
|
|
261
|
+
|
|
262
|
+
- `library.json` - Notebook metadata
|
|
263
|
+
- `auth_info.json` - Authentication status
|
|
264
|
+
- `browser_state/` - Browser cookies and session
|
|
265
|
+
|
|
266
|
+
**Security:** Protected by `.gitignore`, never commit to git.
|
|
267
|
+
|
|
268
|
+
## Resources
|
|
269
|
+
|
|
270
|
+
- **Original Skill**: https://github.com/PleasePrompto/notebooklm-skill
|
|
271
|
+
- **NotebookLM**: https://notebooklm.google.com
|
|
272
|
+
- **Setup Guide**: See `references/setup.md`
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# NotebookLM Skill Setup Guide
|
|
2
|
+
|
|
3
|
+
Complete setup instructions for the NotebookLM skill integration.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
### 1. Python 3.10+
|
|
8
|
+
|
|
9
|
+
Check if Python is installed:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python --version
|
|
13
|
+
# or
|
|
14
|
+
python3 --version
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Install if missing:**
|
|
18
|
+
|
|
19
|
+
**macOS (using Homebrew):**
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
brew install python@3.11
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Ubuntu/Debian:**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
sudo apt update
|
|
29
|
+
sudo apt install python3 python3-pip python3-venv
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Windows:**
|
|
33
|
+
Download from [python.org](https://python.org) and install with "Add to PATH" checked.
|
|
34
|
+
|
|
35
|
+
### 2. Google Chrome
|
|
36
|
+
|
|
37
|
+
Check if Chrome is installed:
|
|
38
|
+
|
|
39
|
+
**macOS:**
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
ls /Applications/Google\ Chrome.app
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Linux:**
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
which google-chrome
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Windows:**
|
|
52
|
+
Check Start Menu for Google Chrome.
|
|
53
|
+
|
|
54
|
+
**Install if missing:**
|
|
55
|
+
Download from [google.com/chrome](https://google.com/chrome)
|
|
56
|
+
|
|
57
|
+
### 3. NotebookLM Account
|
|
58
|
+
|
|
59
|
+
1. Visit [notebooklm.google.com](https://notebooklm.google.com)
|
|
60
|
+
2. Sign in with Google account
|
|
61
|
+
3. Upload documents to create notebooks
|
|
62
|
+
|
|
63
|
+
## First-Time Setup
|
|
64
|
+
|
|
65
|
+
### Step 1: Verify Tool Installation
|
|
66
|
+
|
|
67
|
+
The tool auto-installs the Python skill on first use. Verify it's working:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const setup = await notebooklm({ operation: "setup" });
|
|
71
|
+
console.log(setup.output);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Expected output:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
📦 NotebookLM skill not found. Installing...
|
|
78
|
+
Cloning from https://github.com/PleasePrompto/notebooklm-skill...
|
|
79
|
+
✓ Skill installed successfully
|
|
80
|
+
📍 Location: ~/.opencode/skills/notebooklm
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Step 2: Authenticate with Google
|
|
84
|
+
|
|
85
|
+
**CRITICAL:** Browser must be visible for manual login.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
await notebooklm({
|
|
89
|
+
operation: "auth",
|
|
90
|
+
subOperation: "setup",
|
|
91
|
+
showBrowser: true,
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**What happens:**
|
|
96
|
+
|
|
97
|
+
1. Browser window opens automatically
|
|
98
|
+
2. Navigate to notebooklm.google.com
|
|
99
|
+
3. **You must manually log in** to your Google account
|
|
100
|
+
4. Authentication state is saved for future use
|
|
101
|
+
|
|
102
|
+
**Authentication lasts:** ~7 days (then requires re-auth)
|
|
103
|
+
|
|
104
|
+
### Step 3: Verify Authentication
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const status = await notebooklm({
|
|
108
|
+
operation: "auth",
|
|
109
|
+
subOperation: "status",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log(status.output);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Expected output:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
🔐 Authentication Status:
|
|
119
|
+
Authenticated: Yes
|
|
120
|
+
State age: 0.5 hours
|
|
121
|
+
Last auth: 2025-01-30 14:32:15
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Adding Notebooks
|
|
125
|
+
|
|
126
|
+
### Method 1: Smart Discovery (Recommended)
|
|
127
|
+
|
|
128
|
+
If you don't know the notebook content:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// Step 1: Query to discover content
|
|
132
|
+
const discovery = await notebooklm({
|
|
133
|
+
operation: "query",
|
|
134
|
+
question:
|
|
135
|
+
"What is the content of this notebook? What topics are covered? Provide a complete overview briefly and concisely",
|
|
136
|
+
notebookUrl: "https://notebooklm.google.com/notebook/abc123...",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Step 2: Add with discovered info
|
|
140
|
+
await notebooklm({
|
|
141
|
+
operation: "library",
|
|
142
|
+
subOperation: "add",
|
|
143
|
+
url: "https://notebooklm.google.com/notebook/abc123...",
|
|
144
|
+
name: "API Documentation",
|
|
145
|
+
description: "REST API reference with authentication examples",
|
|
146
|
+
topics: ["api", "rest", "authentication"],
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Method 2: Manual Entry
|
|
151
|
+
|
|
152
|
+
If you already know the details:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
await notebooklm({
|
|
156
|
+
operation: "library",
|
|
157
|
+
subOperation: "add",
|
|
158
|
+
url: "https://notebooklm.google.com/notebook/abc123...",
|
|
159
|
+
name: "Project Documentation",
|
|
160
|
+
description: "Architecture and design decisions",
|
|
161
|
+
topics: ["architecture", "design", "documentation"],
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Usage Examples
|
|
166
|
+
|
|
167
|
+
### Query Active Notebook
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// Set active notebook first
|
|
171
|
+
await notebooklm({
|
|
172
|
+
operation: "library",
|
|
173
|
+
subOperation: "activate",
|
|
174
|
+
notebookId: "api-docs",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Query without specifying notebook
|
|
178
|
+
const answer = await notebooklm({
|
|
179
|
+
operation: "query",
|
|
180
|
+
question: "What are the rate limits?",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
console.log(answer.output);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Query Specific Notebook
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
const answer = await notebooklm({
|
|
190
|
+
operation: "query",
|
|
191
|
+
question: "What are the rate limits?",
|
|
192
|
+
notebookId: "api-docs",
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Query by URL
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const answer = await notebooklm({
|
|
200
|
+
operation: "query",
|
|
201
|
+
question: "What are the rate limits?",
|
|
202
|
+
notebookUrl: "https://notebooklm.google.com/notebook/abc123...",
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### List All Notebooks
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
const library = await notebooklm({
|
|
210
|
+
operation: "library",
|
|
211
|
+
subOperation: "list",
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
console.log(library.output);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Search Notebooks
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const results = await notebooklm({
|
|
221
|
+
operation: "library",
|
|
222
|
+
subOperation: "search",
|
|
223
|
+
query: "authentication",
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
console.log(results.output);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Troubleshooting
|
|
230
|
+
|
|
231
|
+
### "Not authenticated" Error
|
|
232
|
+
|
|
233
|
+
**Cause:** Authentication expired or never completed.
|
|
234
|
+
|
|
235
|
+
**Fix:**
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
await notebooklm({
|
|
239
|
+
operation: "auth",
|
|
240
|
+
subOperation: "setup",
|
|
241
|
+
showBrowser: true,
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### "ModuleNotFoundError" Error
|
|
246
|
+
|
|
247
|
+
**Cause:** Python dependencies not installed.
|
|
248
|
+
|
|
249
|
+
**Fix:** The tool auto-installs on first use. If it fails:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
cd ~/.opencode/skills/notebooklm
|
|
253
|
+
python -m venv .venv
|
|
254
|
+
source .venv/bin/activate # Linux/Mac
|
|
255
|
+
# or .venv\Scripts\activate # Windows
|
|
256
|
+
pip install -r requirements.txt
|
|
257
|
+
python -m patchright install chromium
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### "Notebook not found" Error
|
|
261
|
+
|
|
262
|
+
**Cause:** Notebook ID doesn't exist in library.
|
|
263
|
+
|
|
264
|
+
**Fix:**
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// List available notebooks
|
|
268
|
+
await notebooklm({ operation: "library", subOperation: "list" });
|
|
269
|
+
|
|
270
|
+
// Or add the notebook first
|
|
271
|
+
await notebooklm({
|
|
272
|
+
operation: "library",
|
|
273
|
+
subOperation: "add",
|
|
274
|
+
url: "...",
|
|
275
|
+
name: "...",
|
|
276
|
+
description: "...",
|
|
277
|
+
topics: ["..."],
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Rate Limit (50 queries/day)
|
|
282
|
+
|
|
283
|
+
**Cause:** Free Google account limit reached.
|
|
284
|
+
|
|
285
|
+
**Fix:**
|
|
286
|
+
|
|
287
|
+
- Wait 24 hours
|
|
288
|
+
- Use a different Google account
|
|
289
|
+
- Upgrade to Google One AI Premium (if available)
|
|
290
|
+
|
|
291
|
+
### Browser Not Opening
|
|
292
|
+
|
|
293
|
+
**Cause:** Chrome not found or headless mode enabled.
|
|
294
|
+
|
|
295
|
+
**Fix:**
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// Always use showBrowser: true for auth
|
|
299
|
+
await notebooklm({
|
|
300
|
+
operation: "auth",
|
|
301
|
+
subOperation: "setup",
|
|
302
|
+
showBrowser: true, // Required!
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Data Locations
|
|
307
|
+
|
|
308
|
+
All data stored in `~/.opencode/skills/notebooklm/`:
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
~/.opencode/skills/notebooklm/
|
|
312
|
+
├── data/
|
|
313
|
+
│ ├── library.json # Your notebook library
|
|
314
|
+
│ ├── auth_info.json # Authentication metadata
|
|
315
|
+
│ └── browser_state/ # Browser cookies/session
|
|
316
|
+
│ ├── state.json
|
|
317
|
+
│ └── browser_profile/
|
|
318
|
+
├── scripts/ # Python automation scripts
|
|
319
|
+
│ ├── run.py # Virtual environment wrapper
|
|
320
|
+
│ ├── ask_question.py # Query interface
|
|
321
|
+
│ ├── notebook_manager.py # Library management
|
|
322
|
+
│ ├── auth_manager.py # Authentication
|
|
323
|
+
│ ├── browser_utils.py # Browser utilities
|
|
324
|
+
│ └── config.py # Configuration
|
|
325
|
+
├── .venv/ # Python virtual environment
|
|
326
|
+
├── requirements.txt # Python dependencies
|
|
327
|
+
└── .gitignore # Protects sensitive data
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Security Note:** The `data/` directory contains authentication cookies. Never commit this to git.
|
|
331
|
+
|
|
332
|
+
## Updating the Skill
|
|
333
|
+
|
|
334
|
+
To update to the latest version:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
cd ~/.opencode/skills/notebooklm
|
|
338
|
+
git pull origin master
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Uninstalling
|
|
342
|
+
|
|
343
|
+
To completely remove the skill:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
rm -rf ~/.opencode/skills/notebooklm
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Getting Help
|
|
350
|
+
|
|
351
|
+
- **Original Repository:** https://github.com/PleasePrompto/notebooklm-skill
|
|
352
|
+
- **NotebookLM Help:** https://support.google.com/notebooklm
|
|
353
|
+
- **OpenCodeKit Issues:** File an issue in the opencodekit-template repository
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotebookLM Tool for OpenCodeKit
|
|
3
|
+
*
|
|
4
|
+
* Wraps the Python NotebookLM skill for querying Google NotebookLM notebooks.
|
|
5
|
+
* Auto-clones the skill repository and manages the Python virtual environment.
|
|
6
|
+
*
|
|
7
|
+
* @module notebooklm
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { $ } from "bun";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Logger interface for OpenCode client app logging
|
|
16
|
+
* Falls back to console if client logger not available
|
|
17
|
+
*/
|
|
18
|
+
interface Logger {
|
|
19
|
+
debug: (message: string, extra?: Record<string, unknown>) => void;
|
|
20
|
+
info: (message: string, extra?: Record<string, unknown>) => void;
|
|
21
|
+
warn: (message: string, extra?: Record<string, unknown>) => void;
|
|
22
|
+
error: (message: string, extra?: Record<string, unknown>) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a logger that uses OpenCode client.app.log() when available,
|
|
27
|
+
* otherwise falls back to console
|
|
28
|
+
*/
|
|
29
|
+
function createLogger(client?: {
|
|
30
|
+
app: {
|
|
31
|
+
log: (log: {
|
|
32
|
+
service: string;
|
|
33
|
+
level: "debug" | "info" | "warn" | "error";
|
|
34
|
+
message: string;
|
|
35
|
+
extra?: Record<string, unknown>;
|
|
36
|
+
}) => Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
}): Logger {
|
|
39
|
+
const service = "notebooklm";
|
|
40
|
+
|
|
41
|
+
if (client?.app?.log) {
|
|
42
|
+
// Use OpenCode client logging
|
|
43
|
+
return {
|
|
44
|
+
debug: (message: string, extra?: Record<string, unknown>) => {
|
|
45
|
+
client.app
|
|
46
|
+
.log({ service, level: "debug", message, extra })
|
|
47
|
+
.catch(() => {});
|
|
48
|
+
},
|
|
49
|
+
info: (message: string, extra?: Record<string, unknown>) => {
|
|
50
|
+
client.app
|
|
51
|
+
.log({ service, level: "info", message, extra })
|
|
52
|
+
.catch(() => {});
|
|
53
|
+
},
|
|
54
|
+
warn: (message: string, extra?: Record<string, unknown>) => {
|
|
55
|
+
client.app
|
|
56
|
+
.log({ service, level: "warn", message, extra })
|
|
57
|
+
.catch(() => {});
|
|
58
|
+
},
|
|
59
|
+
error: (message: string, extra?: Record<string, unknown>) => {
|
|
60
|
+
client.app
|
|
61
|
+
.log({ service, level: "error", message, extra })
|
|
62
|
+
.catch(() => {});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Fallback to console
|
|
68
|
+
return {
|
|
69
|
+
debug: (message: string, extra?: Record<string, unknown>) => {
|
|
70
|
+
console.log(`[DEBUG] ${message}`, extra ? JSON.stringify(extra) : "");
|
|
71
|
+
},
|
|
72
|
+
info: (message: string, extra?: Record<string, unknown>) => {
|
|
73
|
+
console.log(`[INFO] ${message}`, extra ? JSON.stringify(extra) : "");
|
|
74
|
+
},
|
|
75
|
+
warn: (message: string, extra?: Record<string, unknown>) => {
|
|
76
|
+
console.warn(`[WARN] ${message}`, extra ? JSON.stringify(extra) : "");
|
|
77
|
+
},
|
|
78
|
+
error: (message: string, extra?: Record<string, unknown>) => {
|
|
79
|
+
console.error(`[ERROR] ${message}`, extra ? JSON.stringify(extra) : "");
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Global logger instance (will be initialized with client if available)
|
|
85
|
+
let globalLogger: Logger = createLogger();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Set the logger instance (called by plugin initialization)
|
|
89
|
+
*/
|
|
90
|
+
export function setNotebookLMLogger(client?: {
|
|
91
|
+
app: {
|
|
92
|
+
log: (log: {
|
|
93
|
+
service: string;
|
|
94
|
+
level: "debug" | "info" | "warn" | "error";
|
|
95
|
+
message: string;
|
|
96
|
+
extra?: Record<string, unknown>;
|
|
97
|
+
}) => Promise<void>;
|
|
98
|
+
};
|
|
99
|
+
}): void {
|
|
100
|
+
globalLogger = createLogger(client);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Detect Python command (python or python3)
|
|
104
|
+
async function getPythonCommand(): Promise<string> {
|
|
105
|
+
try {
|
|
106
|
+
await $`python --version`.quiet();
|
|
107
|
+
return "python";
|
|
108
|
+
} catch {
|
|
109
|
+
try {
|
|
110
|
+
await $`python3 --version`.quiet();
|
|
111
|
+
return "python3";
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error("Python not found. Please install Python 3.10+");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Skill repository URL
|
|
119
|
+
const SKILL_REPO = "https://github.com/PleasePrompto/notebooklm-skill";
|
|
120
|
+
|
|
121
|
+
// Skill installation directory
|
|
122
|
+
const SKILL_DIR = join(homedir(), ".opencode", "skills", "notebooklm");
|
|
123
|
+
|
|
124
|
+
// Scripts directory
|
|
125
|
+
const SCRIPTS_DIR = join(SKILL_DIR, "scripts");
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Input parameters for notebooklm tool
|
|
129
|
+
*/
|
|
130
|
+
export interface NotebookLMInput {
|
|
131
|
+
/** Operation to perform */
|
|
132
|
+
operation: "auth" | "query" | "library" | "setup";
|
|
133
|
+
|
|
134
|
+
/** Sub-operation for auth and library operations */
|
|
135
|
+
subOperation?:
|
|
136
|
+
| "setup"
|
|
137
|
+
| "status"
|
|
138
|
+
| "reauth"
|
|
139
|
+
| "clear"
|
|
140
|
+
| "validate"
|
|
141
|
+
| "add"
|
|
142
|
+
| "list"
|
|
143
|
+
| "search"
|
|
144
|
+
| "activate"
|
|
145
|
+
| "remove"
|
|
146
|
+
| "stats";
|
|
147
|
+
|
|
148
|
+
/** Question to ask (for query operation) */
|
|
149
|
+
question?: string;
|
|
150
|
+
|
|
151
|
+
/** Notebook URL (for add or query operations) */
|
|
152
|
+
notebookUrl?: string;
|
|
153
|
+
|
|
154
|
+
/** Notebook ID from library (for query or activate operations) */
|
|
155
|
+
notebookId?: string;
|
|
156
|
+
|
|
157
|
+
/** Notebook name (for add operation) */
|
|
158
|
+
name?: string;
|
|
159
|
+
|
|
160
|
+
/** Notebook description (for add operation) */
|
|
161
|
+
description?: string;
|
|
162
|
+
|
|
163
|
+
/** Topics/tags (for add operation) - array or comma-separated string */
|
|
164
|
+
topics?: string[] | string;
|
|
165
|
+
|
|
166
|
+
/** Search query (for library search) */
|
|
167
|
+
query?: string;
|
|
168
|
+
|
|
169
|
+
/** Show browser window (for auth setup or debugging) */
|
|
170
|
+
showBrowser?: boolean;
|
|
171
|
+
|
|
172
|
+
/** Timeout in minutes for authentication */
|
|
173
|
+
timeout?: number;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Result from notebooklm operation
|
|
178
|
+
*/
|
|
179
|
+
export interface NotebookLMResult {
|
|
180
|
+
/** Whether the operation succeeded */
|
|
181
|
+
success: boolean;
|
|
182
|
+
|
|
183
|
+
/** Output from the operation */
|
|
184
|
+
output: string;
|
|
185
|
+
|
|
186
|
+
/** Error message if failed */
|
|
187
|
+
error?: string;
|
|
188
|
+
|
|
189
|
+
/** Parsed data (for library operations) */
|
|
190
|
+
data?: unknown;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Ensure Python virtual environment is set up with dependencies
|
|
195
|
+
*/
|
|
196
|
+
async function ensureVirtualEnv(): Promise<void> {
|
|
197
|
+
const venvPath = join(SKILL_DIR, ".venv");
|
|
198
|
+
const venvPython = join(venvPath, "bin", "python");
|
|
199
|
+
const venvPip = join(venvPath, "bin", "pip");
|
|
200
|
+
|
|
201
|
+
// Check if venv exists with pip
|
|
202
|
+
const venvExists = await Bun.file(venvPython).exists();
|
|
203
|
+
const pipExists = await Bun.file(venvPip).exists();
|
|
204
|
+
|
|
205
|
+
if (venvExists && pipExists) {
|
|
206
|
+
// Check if patchright is installed
|
|
207
|
+
const checkResult =
|
|
208
|
+
await $`cd ${SKILL_DIR} && .venv/bin/python -c "import patchright" 2>&1`.nothrow();
|
|
209
|
+
if (checkResult.exitCode === 0) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
globalLogger.info("Setting up Python virtual environment...");
|
|
215
|
+
|
|
216
|
+
const pythonCmd = await getPythonCommand();
|
|
217
|
+
|
|
218
|
+
// Remove broken venv if exists
|
|
219
|
+
if (venvExists && !pipExists) {
|
|
220
|
+
globalLogger.warn("Removing incomplete virtual environment...");
|
|
221
|
+
await $`cd ${SKILL_DIR} && rm -rf .venv`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Create virtual environment with pip
|
|
225
|
+
globalLogger.info("Creating virtual environment...");
|
|
226
|
+
const venvResult =
|
|
227
|
+
await $`cd ${SKILL_DIR} && ${pythonCmd} -m venv .venv --without-pip`.nothrow();
|
|
228
|
+
if (venvResult.exitCode !== 0) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Failed to create virtual environment: ${venvResult.stderr}`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Install pip manually
|
|
235
|
+
globalLogger.info("Installing pip...");
|
|
236
|
+
const pipInstallResult =
|
|
237
|
+
await $`cd ${SKILL_DIR} && .venv/bin/python -m ensurepip --upgrade`.nothrow();
|
|
238
|
+
if (pipInstallResult.exitCode !== 0) {
|
|
239
|
+
// Try alternative method
|
|
240
|
+
await $`cd ${SKILL_DIR} && curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && .venv/bin/python get-pip.py && rm get-pip.py`.nothrow();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Install dependencies
|
|
244
|
+
globalLogger.info("Installing dependencies (this may take a minute)...");
|
|
245
|
+
const pipResult =
|
|
246
|
+
await $`cd ${SKILL_DIR} && .venv/bin/python -m pip install -r requirements.txt`.nothrow();
|
|
247
|
+
if (pipResult.exitCode !== 0) {
|
|
248
|
+
throw new Error(`Failed to install dependencies: ${pipResult.stderr}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Install patchright browser
|
|
252
|
+
globalLogger.info("Installing browser automation...");
|
|
253
|
+
const browserResult =
|
|
254
|
+
await $`cd ${SKILL_DIR} && .venv/bin/python -m patchright install chromium`.nothrow();
|
|
255
|
+
if (browserResult.exitCode !== 0) {
|
|
256
|
+
globalLogger.warn("Browser install may have issues, but continuing...");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
globalLogger.info("Virtual environment ready");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Ensure the NotebookLM skill is installed
|
|
264
|
+
*/
|
|
265
|
+
async function ensureSkillInstalled(): Promise<void> {
|
|
266
|
+
const runPyPath = join(SCRIPTS_DIR, "run.py");
|
|
267
|
+
|
|
268
|
+
// Check if skill is already installed
|
|
269
|
+
const exists = await Bun.file(runPyPath).exists();
|
|
270
|
+
if (exists) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
globalLogger.info("NotebookLM skill not found. Installing...");
|
|
275
|
+
|
|
276
|
+
// Create parent directory
|
|
277
|
+
const parentDir = join(homedir(), ".opencode", "skills");
|
|
278
|
+
await $`mkdir -p ${parentDir}`;
|
|
279
|
+
|
|
280
|
+
// Clone the repository
|
|
281
|
+
globalLogger.info(`Cloning from ${SKILL_REPO}...`);
|
|
282
|
+
const result =
|
|
283
|
+
await $`cd ${parentDir} && git clone ${SKILL_REPO} notebooklm`.quiet();
|
|
284
|
+
|
|
285
|
+
if (result.exitCode !== 0) {
|
|
286
|
+
throw new Error(`Failed to clone NotebookLM skill: ${result.stderr}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
globalLogger.info("Skill installed successfully", { location: SKILL_DIR });
|
|
290
|
+
|
|
291
|
+
// Set up virtual environment
|
|
292
|
+
await ensureVirtualEnv();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Build command arguments for run.py wrapper
|
|
297
|
+
*/
|
|
298
|
+
function buildCommandArgs(input: NotebookLMInput): string[] {
|
|
299
|
+
const args: string[] = [];
|
|
300
|
+
|
|
301
|
+
switch (input.operation) {
|
|
302
|
+
case "auth":
|
|
303
|
+
args.push(input.subOperation || "status");
|
|
304
|
+
|
|
305
|
+
if (input.subOperation === "setup" || input.subOperation === "reauth") {
|
|
306
|
+
if (!input.showBrowser) {
|
|
307
|
+
args.push("--headless");
|
|
308
|
+
}
|
|
309
|
+
if (input.timeout) {
|
|
310
|
+
args.push("--timeout", String(input.timeout));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
|
|
315
|
+
case "library":
|
|
316
|
+
args.push(input.subOperation || "list");
|
|
317
|
+
|
|
318
|
+
if (input.subOperation === "add") {
|
|
319
|
+
if (input.notebookUrl) args.push("--url", input.notebookUrl);
|
|
320
|
+
if (input.name) args.push("--name", input.name);
|
|
321
|
+
if (input.description) args.push("--description", input.description);
|
|
322
|
+
if (input.topics) {
|
|
323
|
+
const topicsStr = Array.isArray(input.topics)
|
|
324
|
+
? input.topics.join(",")
|
|
325
|
+
: input.topics;
|
|
326
|
+
args.push("--topics", topicsStr);
|
|
327
|
+
}
|
|
328
|
+
} else if (input.subOperation === "search") {
|
|
329
|
+
if (input.query) args.push("--query", input.query);
|
|
330
|
+
} else if (
|
|
331
|
+
input.subOperation === "activate" ||
|
|
332
|
+
input.subOperation === "remove"
|
|
333
|
+
) {
|
|
334
|
+
if (input.notebookId) args.push("--id", input.notebookId);
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
|
|
338
|
+
case "query":
|
|
339
|
+
// ask_question.py is passed as script parameter
|
|
340
|
+
|
|
341
|
+
if (input.question) {
|
|
342
|
+
args.push("--question", input.question);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (input.notebookUrl) {
|
|
346
|
+
args.push("--notebook-url", input.notebookUrl);
|
|
347
|
+
} else if (input.notebookId) {
|
|
348
|
+
args.push("--notebook-id", input.notebookId);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (input.showBrowser) {
|
|
352
|
+
args.push("--show-browser");
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
|
|
356
|
+
case "setup":
|
|
357
|
+
// Setup is handled by ensureSkillInstalled
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return args;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Execute a NotebookLM operation
|
|
366
|
+
*/
|
|
367
|
+
async function executeOperation(
|
|
368
|
+
script: string,
|
|
369
|
+
args: string[],
|
|
370
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
371
|
+
const runPyPath = join(SCRIPTS_DIR, "run.py");
|
|
372
|
+
const venvPython = join(SKILL_DIR, ".venv", "bin", "python");
|
|
373
|
+
|
|
374
|
+
// Use venv python if available, otherwise fall back to system
|
|
375
|
+
const pythonCmd = (await Bun.file(venvPython).exists())
|
|
376
|
+
? venvPython
|
|
377
|
+
: await getPythonCommand();
|
|
378
|
+
|
|
379
|
+
// Build full command
|
|
380
|
+
const cmd = [pythonCmd, runPyPath, script, ...args];
|
|
381
|
+
|
|
382
|
+
globalLogger.debug(`Executing: ${cmd.join(" ")}`);
|
|
383
|
+
|
|
384
|
+
// Execute with Bun
|
|
385
|
+
const result = await $`cd ${SKILL_DIR} && ${cmd}`.nothrow();
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
stdout: result.stdout.toString(),
|
|
389
|
+
stderr: result.stderr.toString(),
|
|
390
|
+
exitCode: result.exitCode,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Main notebooklm tool function
|
|
396
|
+
*
|
|
397
|
+
* @param input - Operation parameters
|
|
398
|
+
* @returns Result of the operation
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* // Check authentication status
|
|
402
|
+
* const status = await notebooklm({ operation: "auth", subOperation: "status" });
|
|
403
|
+
*
|
|
404
|
+
* @example
|
|
405
|
+
* // Query a notebook
|
|
406
|
+
* const answer = await notebooklm({
|
|
407
|
+
* operation: "query",
|
|
408
|
+
* question: "What are the key findings?",
|
|
409
|
+
* notebookId: "my-notebook"
|
|
410
|
+
* });
|
|
411
|
+
*/
|
|
412
|
+
export default async function notebooklm(
|
|
413
|
+
input: NotebookLMInput,
|
|
414
|
+
): Promise<NotebookLMResult> {
|
|
415
|
+
try {
|
|
416
|
+
// Ensure skill is installed
|
|
417
|
+
await ensureSkillInstalled();
|
|
418
|
+
|
|
419
|
+
// Ensure virtual environment is set up (for non-setup operations)
|
|
420
|
+
if (input.operation !== "setup") {
|
|
421
|
+
await ensureVirtualEnv();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Handle setup operation (just verify installation)
|
|
425
|
+
if (input.operation === "setup") {
|
|
426
|
+
return {
|
|
427
|
+
success: true,
|
|
428
|
+
output: `NotebookLM skill installed at: ${SKILL_DIR}\n\nPrerequisites:\n- Python 3.10+ must be installed\n- Google Chrome must be installed\n- Run auth setup before querying`,
|
|
429
|
+
data: { skillDir: SKILL_DIR },
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Build command arguments
|
|
434
|
+
const args = buildCommandArgs(input);
|
|
435
|
+
|
|
436
|
+
// Determine which script to run
|
|
437
|
+
let script: string;
|
|
438
|
+
switch (input.operation) {
|
|
439
|
+
case "auth":
|
|
440
|
+
script = "auth_manager.py";
|
|
441
|
+
break;
|
|
442
|
+
case "library":
|
|
443
|
+
script = "notebook_manager.py";
|
|
444
|
+
break;
|
|
445
|
+
case "query":
|
|
446
|
+
script = "ask_question.py";
|
|
447
|
+
break;
|
|
448
|
+
default:
|
|
449
|
+
throw new Error(`Unknown operation: ${input.operation}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Execute the operation
|
|
453
|
+
globalLogger.info(
|
|
454
|
+
`NotebookLM: ${input.operation}${input.subOperation ? `/${input.subOperation}` : ""}`,
|
|
455
|
+
);
|
|
456
|
+
const result = await executeOperation(script, args);
|
|
457
|
+
|
|
458
|
+
// Parse result
|
|
459
|
+
const success = result.exitCode === 0;
|
|
460
|
+
|
|
461
|
+
// Try to parse JSON data from output (for library operations)
|
|
462
|
+
let data: unknown = undefined;
|
|
463
|
+
try {
|
|
464
|
+
// Look for JSON in the output
|
|
465
|
+
const jsonMatch = result.stdout.match(/\{[\s\S]*\}/);
|
|
466
|
+
if (jsonMatch) {
|
|
467
|
+
data = JSON.parse(jsonMatch[0]);
|
|
468
|
+
}
|
|
469
|
+
} catch {
|
|
470
|
+
// Not JSON, that's fine
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
success,
|
|
475
|
+
output: result.stdout || result.stderr,
|
|
476
|
+
error: success ? undefined : result.stderr,
|
|
477
|
+
data,
|
|
478
|
+
};
|
|
479
|
+
} catch (error) {
|
|
480
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
success: false,
|
|
484
|
+
output: "",
|
|
485
|
+
error: errorMessage,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
package/package.json
CHANGED