Skip to content

Commit b792af6

Browse files
feat(help-skill): expose debug.log for agent self-diagnosis (#698)
* feat(help-skill): expose debug.log to gemini-scribe-help for self-diagnosis Fixes #586 Add debug.log and debug.log.old as virtual resources on the bundled gemini-scribe-help skill, gated on the fileLogging setting and on each file actually existing on disk. The plugin state folder is otherwise blocked by the standard read_file tool, so this skill is the supported path for the agent to read its own logs when diagnosing user-reported issues. Also rewrite the help-skill description so diagnostic-language prompts ("error", "bug", "what just happened", "check the debug log") trigger activate_skill directly, instead of the agent first wandering through find_files_by_name / read_file on the state folder. * docs(help-skill): broaden 'Resource not found' guidance Address CodeRabbit feedback on #698: the message can also surface when file logging is on but the log file does not exist yet (e.g. debug.log.old before any rotation), not only when Log to File is disabled.
1 parent b797e31 commit b792af6

4 files changed

Lines changed: 213 additions & 8 deletions

File tree

docs/reference/settings.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,8 @@ Advanced settings for developers and power users. Access by clicking "Show Advan
193193
- Debug-level entries (`log()`, `debug()`) are only written when Debug Mode is also enabled
194194
- Log files are automatically rotated at 1 MB (previous log kept as `debug.log.old`)
195195
- Writes are batched and debounced to minimize I/O impact
196-
- **Use case**: Sharing diagnostic information in bug reports, or enabling the agent to read logs for self-diagnosis via vault tools
197-
- **Note**: Log files are stored in the plugin state folder and are automatically excluded from RAG indexing
196+
- **Use case**: Sharing diagnostic information in bug reports, or letting the agent self-diagnose issues via the bundled `gemini-scribe-help` skill (which exposes `debug.log` and `debug.log.old` as activatable resources only when this setting is on)
197+
- **Note**: Log files are stored in the plugin state folder and are automatically excluded from RAG indexing. The standard `read_file` tool blocks the state folder; the help skill is the supported path for the agent to read these logs.
198198

199199
### API Configuration
200200

prompts/bundled-skills/gemini-scribe-help/SKILL.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: gemini-scribe-help
3-
description: Answer questions about Gemini Scribe plugin features, settings, and usage. Activate this skill when users ask how to use the plugin, configure settings, or troubleshoot issues.
3+
description: Answer questions about Gemini Scribe plugin features, settings, and usage, and diagnose plugin errors by reading the user's debug.log (when File Logging is enabled). Activate this skill whenever the user asks how to use the plugin or configure settings, OR reports that something went wrong with the plugin — bug, error, crash, broken behavior, "not working", "what just happened" — especially when they mention the debug log, log file, console output, or want help troubleshooting. Always activate this skill before searching the vault for plugin log files; debug.log lives in the plugin state folder which the standard read_file tool blocks.
44
---
55

66
# Gemini Scribe Help
@@ -24,3 +24,14 @@ User asks: "How do I set up inline completions?"
2424

2525
1. Load `references/completions.md` via `activate_skill(name: "gemini-scribe-help", resource_path: "references/completions.md")`
2626
2. Answer using the loaded content
27+
28+
## Troubleshooting with Debug Logs
29+
30+
When a user reports a bug or unexpected behavior, check whether they have **Log to File** enabled (Settings → Developer → Log to File). When enabled, two additional resources are available on this skill:
31+
32+
- `debug.log` — current log file
33+
- `debug.log.old` — previous rotated log (present only after a rotation)
34+
35+
Load them the same way as a reference, e.g. `activate_skill(name: "gemini-scribe-help", resource_path: "debug.log")`. Each entry is timestamped with a severity level (`LOG`, `DEBUG`, `ERROR`, `WARN`); focus on `ERROR` and `WARN` entries near the time the user encountered the issue.
36+
37+
If `activate_skill` returns "Resource not found" for these paths, Log to File may be disabled, or the log file may not exist yet (e.g. `debug.log.old` is only present after a rotation). Ask the user to enable Log to File (and Debug Mode for verbose entries), reproduce the issue once, then re-check.

src/services/skill-manager.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ const SKILL_NAME_REGEX = /^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]$/;
3434
const SKILL_NAME_MAX_LENGTH = 64;
3535
const SKILL_MD_FILENAME = 'SKILL.md';
3636

37+
/**
38+
* The bundled help skill exposes the plugin's debug log files as virtual
39+
* resources when file logging is enabled, so the agent can self-diagnose
40+
* user-reported issues. These files live in the plugin state folder, which
41+
* the standard read_file tool blocks.
42+
*/
43+
const HELP_SKILL_NAME = 'gemini-scribe-help';
44+
const HELP_DEBUG_LOG_RESOURCES = ['debug.log', 'debug.log.old'] as const;
45+
type HelpDebugLogResource = (typeof HELP_DEBUG_LOG_RESOURCES)[number];
46+
47+
function isHelpDebugLogResource(path: string): path is HelpDebugLogResource {
48+
return (HELP_DEBUG_LOG_RESOURCES as readonly string[]).includes(path);
49+
}
50+
3751
/**
3852
* Find the character offset of the closing YAML frontmatter delimiter in a file's content.
3953
* Returns the offset immediately AFTER the closing delimiter token (`---` or `...`)
@@ -220,6 +234,11 @@ export class SkillManager {
220234
return null;
221235
}
222236

237+
// Help skill exposes debug.log / debug.log.old as virtual resources.
238+
if (skillName === HELP_SKILL_NAME && isHelpDebugLogResource(relativePath)) {
239+
return await this.readHelpDebugLog(relativePath);
240+
}
241+
223242
const resourcePath = normalizePath(`${this.getSkillsFolderPath()}/${skillName}/${relativePath}`);
224243

225244
// Verify resolved path stays within the skill directory
@@ -238,6 +257,24 @@ export class SkillManager {
238257
return await this.plugin.app.vault.read(file);
239258
}
240259

260+
/**
261+
* Read a debug log file from the plugin state folder for the help skill.
262+
* Returns null when file logging is disabled or the log file is absent.
263+
*/
264+
private async readHelpDebugLog(filename: HelpDebugLogResource): Promise<string | null> {
265+
if (!this.plugin.settings?.fileLogging) return null;
266+
const adapter = this.plugin.app?.vault?.adapter;
267+
if (!adapter) return null;
268+
const path = normalizePath(`${this.plugin.settings.historyFolder}/${filename}`);
269+
try {
270+
if (!(await adapter.exists(path))) return null;
271+
return await adapter.read(path);
272+
} catch (error) {
273+
this.plugin.logger.warn(`Failed to read debug log "${path}":`, error);
274+
return null;
275+
}
276+
}
277+
241278
/**
242279
* List available resources within a skill directory
243280
*/
@@ -251,16 +288,43 @@ export class SkillManager {
251288
const skillDir = normalizePath(`${this.getSkillsFolderPath()}/${skillName}`);
252289
const folder = this.plugin.app.vault.getAbstractFileByPath(skillDir);
253290

254-
if (!(folder instanceof TFolder)) {
291+
let resources: string[];
292+
if (folder instanceof TFolder) {
293+
resources = [];
294+
this.collectFiles(folder, skillDir, resources);
295+
} else {
255296
// Fall back to bundled skill resources
256-
return BundledSkillRegistry.listResources(skillName);
297+
resources = [...BundledSkillRegistry.listResources(skillName)];
298+
}
299+
300+
// Help skill exposes debug.log / debug.log.old as virtual resources
301+
// when the user has file logging enabled and a log file exists.
302+
if (skillName === HELP_SKILL_NAME) {
303+
const debugLogs = await this.listHelpDebugLogResources();
304+
for (const name of debugLogs) {
305+
if (!resources.includes(name)) resources.push(name);
306+
}
257307
}
258308

259-
const resources: string[] = [];
260-
this.collectFiles(folder, skillDir, resources);
261309
return resources;
262310
}
263311

312+
private async listHelpDebugLogResources(): Promise<HelpDebugLogResource[]> {
313+
if (!this.plugin.settings?.fileLogging) return [];
314+
const adapter = this.plugin.app?.vault?.adapter;
315+
if (!adapter) return [];
316+
const present: HelpDebugLogResource[] = [];
317+
for (const name of HELP_DEBUG_LOG_RESOURCES) {
318+
const path = normalizePath(`${this.plugin.settings.historyFolder}/${name}`);
319+
try {
320+
if (await adapter.exists(path)) present.push(name);
321+
} catch {
322+
// Treat probe failures as "not present"; never throw from listing.
323+
}
324+
}
325+
return present;
326+
}
327+
264328
/**
265329
* Recursively collect file paths relative to a base directory
266330
*/

test/services/skill-manager.test.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ const mockVault = {
7171
create: jest.fn(),
7272
read: jest.fn(),
7373
getMarkdownFiles: jest.fn(),
74-
adapter: { exists: jest.fn().mockResolvedValue(false) },
74+
adapter: {
75+
exists: jest.fn().mockResolvedValue(false),
76+
read: jest.fn().mockResolvedValue(''),
77+
},
7578
};
7679

7780
const mockMetadataCache = {
@@ -104,6 +107,11 @@ describe('SkillManager', () => {
104107

105108
beforeEach(() => {
106109
jest.clearAllMocks();
110+
// clearAllMocks resets call history but keeps any custom implementations
111+
// from prior tests, so re-pin adapter defaults explicitly.
112+
mockVault.adapter.exists.mockReset().mockResolvedValue(false);
113+
mockVault.adapter.read.mockReset().mockResolvedValue('');
114+
mockPlugin.settings.fileLogging = false;
107115
manager = new SkillManager(mockPlugin);
108116
});
109117

@@ -266,6 +274,128 @@ describe('SkillManager', () => {
266274
const content = await manager.readSkillResource('my-skill', '/etc/passwd');
267275
expect(content).toBeNull();
268276
});
277+
278+
describe('help skill debug log virtual resources', () => {
279+
beforeEach(() => {
280+
// readHelpDebugLog goes through vault.adapter, not getAbstractFileByPath.
281+
mockVault.getAbstractFileByPath.mockReturnValue(null);
282+
});
283+
284+
it('should read debug.log via the help skill when fileLogging is on', async () => {
285+
mockPlugin.settings.fileLogging = true;
286+
mockVault.adapter.exists.mockImplementation(async (path: string) => path === 'gemini-scribe/debug.log');
287+
mockVault.adapter.read.mockResolvedValue('[2026-04-25T10:00:00] [ERROR] [Gemini Scribe] boom');
288+
289+
const content = await manager.readSkillResource('gemini-scribe-help', 'debug.log');
290+
291+
expect(content).toBe('[2026-04-25T10:00:00] [ERROR] [Gemini Scribe] boom');
292+
expect(mockVault.adapter.read).toHaveBeenCalledWith('gemini-scribe/debug.log');
293+
});
294+
295+
it('should read debug.log.old when present', async () => {
296+
mockPlugin.settings.fileLogging = true;
297+
mockVault.adapter.exists.mockImplementation(async (path: string) => path === 'gemini-scribe/debug.log.old');
298+
mockVault.adapter.read.mockResolvedValue('rotated content');
299+
300+
const content = await manager.readSkillResource('gemini-scribe-help', 'debug.log.old');
301+
302+
expect(content).toBe('rotated content');
303+
expect(mockVault.adapter.read).toHaveBeenCalledWith('gemini-scribe/debug.log.old');
304+
});
305+
306+
it('should return null when fileLogging is disabled', async () => {
307+
mockPlugin.settings.fileLogging = false;
308+
mockVault.adapter.exists.mockResolvedValue(true);
309+
310+
const content = await manager.readSkillResource('gemini-scribe-help', 'debug.log');
311+
312+
expect(content).toBeNull();
313+
// Must not even probe disk when the user has opted out.
314+
expect(mockVault.adapter.exists).not.toHaveBeenCalled();
315+
expect(mockVault.adapter.read).not.toHaveBeenCalled();
316+
});
317+
318+
it('should return null when log file does not exist', async () => {
319+
mockPlugin.settings.fileLogging = true;
320+
mockVault.adapter.exists.mockResolvedValue(false);
321+
322+
const content = await manager.readSkillResource('gemini-scribe-help', 'debug.log');
323+
324+
expect(content).toBeNull();
325+
expect(mockVault.adapter.read).not.toHaveBeenCalled();
326+
});
327+
328+
it('should not expose debug.log on other skills', async () => {
329+
mockPlugin.settings.fileLogging = true;
330+
mockVault.adapter.exists.mockResolvedValue(true);
331+
mockVault.adapter.read.mockResolvedValue('should not be returned');
332+
333+
const content = await manager.readSkillResource('gemini-scribe', 'debug.log');
334+
335+
expect(content).toBeNull();
336+
expect(mockVault.adapter.read).not.toHaveBeenCalled();
337+
});
338+
339+
it('should still reject path traversal even for the help skill', async () => {
340+
mockPlugin.settings.fileLogging = true;
341+
const content = await manager.readSkillResource('gemini-scribe-help', '../debug.log');
342+
expect(content).toBeNull();
343+
});
344+
});
345+
});
346+
347+
describe('listSkillResources', () => {
348+
beforeEach(() => {
349+
// Default: no vault skill dir, no debug log files.
350+
mockVault.getAbstractFileByPath.mockReturnValue(null);
351+
mockVault.adapter.exists.mockResolvedValue(false);
352+
});
353+
354+
it('should fall through to bundled resources when no vault folder', async () => {
355+
const resources = await manager.listSkillResources('gemini-scribe-help');
356+
expect(resources).toEqual(expect.arrayContaining(['references/agent-mode.md', 'references/settings.md']));
357+
});
358+
359+
it('should append debug.log resources for the help skill when fileLogging is on and files exist', async () => {
360+
mockPlugin.settings.fileLogging = true;
361+
mockVault.adapter.exists.mockImplementation(
362+
async (path: string) => path === 'gemini-scribe/debug.log' || path === 'gemini-scribe/debug.log.old'
363+
);
364+
365+
const resources = await manager.listSkillResources('gemini-scribe-help');
366+
367+
expect(resources).toEqual(expect.arrayContaining(['debug.log', 'debug.log.old']));
368+
});
369+
370+
it('should only include logs that exist on disk', async () => {
371+
mockPlugin.settings.fileLogging = true;
372+
mockVault.adapter.exists.mockImplementation(async (path: string) => path === 'gemini-scribe/debug.log');
373+
374+
const resources = await manager.listSkillResources('gemini-scribe-help');
375+
376+
expect(resources).toContain('debug.log');
377+
expect(resources).not.toContain('debug.log.old');
378+
});
379+
380+
it('should omit debug logs when fileLogging is disabled', async () => {
381+
mockPlugin.settings.fileLogging = false;
382+
mockVault.adapter.exists.mockResolvedValue(true);
383+
384+
const resources = await manager.listSkillResources('gemini-scribe-help');
385+
386+
expect(resources).not.toContain('debug.log');
387+
expect(resources).not.toContain('debug.log.old');
388+
});
389+
390+
it('should not add debug logs to other skills', async () => {
391+
mockPlugin.settings.fileLogging = true;
392+
mockVault.adapter.exists.mockResolvedValue(true);
393+
394+
const resources = await manager.listSkillResources('obsidian-bases');
395+
396+
expect(resources).not.toContain('debug.log');
397+
expect(resources).not.toContain('debug.log.old');
398+
});
269399
});
270400

271401
describe('getSkillSummaries', () => {

0 commit comments

Comments
 (0)