mirror of
https://github.com/lossless-group/perplexed-plugin.git
synced 2026-07-22 06:49:50 +00:00
feat(seeder): auto-deploy shipped templates + README to user vault on first run
A freshly-installed perplexed has no templates folder, so the directory-template commands have nothing to match. Users had to copy templates from the plugin source by hand. Now the four shipped templates (concept-, vocabulary-, source-, toolkit-profile) plus a README are inlined into main.js via esbuild's text loader for .md files, and a seeder writes them into the configured templates root on plugin load — but only if the folder is missing or contains no markdown, so existing customizations are never touched. A new "Re-seed templates" button in the Directory templates settings section fills in any shipped file whose filename doesn't already exist (for pulling in new templates after a plugin update). Pieces: - esbuild.config.mjs: register `.md` text loader so markdown imports work - src/types/markdown.d.ts: TS shim for `*.md` imports - src/services/templateSeederService.ts: seedTemplatesIfMissing + reSeedMissingFiles (idempotent, never overwrites) - src/docs/templates/README.md: end-user docs for the template system (zone anatomy, interpolation tokens, commands, image markers, citation behavior, frontmatter stamps, writing a custom template) - main.ts: seeder fires after settings load; settings tab gains a Re-seed button Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9af617f38c
commit
3349e36bd6
5 changed files with 277 additions and 2 deletions
|
|
@ -54,7 +54,7 @@ const context = await esbuild.context({
|
|||
},
|
||||
logLevel: 'info',
|
||||
outfile: 'main.js',
|
||||
loader: { '.css': 'text' },
|
||||
loader: { '.css': 'text', '.md': 'text' },
|
||||
});
|
||||
|
||||
if (isProduction) {
|
||||
|
|
|
|||
31
main.ts
31
main.ts
|
|
@ -33,6 +33,7 @@ import type { DirectoryTemplateSettings, ParsedTemplate } from './src/services/d
|
|||
import type { TFile } from 'obsidian';
|
||||
import { findImagesForSelection } from './src/services/findImagesService';
|
||||
import type { FindImagesSettings } from './src/services/findImagesService';
|
||||
import { reSeedMissingFiles, seedTemplatesIfMissing } from './src/services/templateSeederService';
|
||||
|
||||
|
||||
interface PerplexedPluginSettings {
|
||||
|
|
@ -329,7 +330,20 @@ export default class PerplexedPlugin extends Plugin {
|
|||
|
||||
await this.loadSettings();
|
||||
console.debug('Perplexed Plugin: Settings loaded successfully');
|
||||
|
||||
|
||||
// First-run seeding: if the configured templates root is missing
|
||||
// or empty, drop in the four shipped templates plus a README so a
|
||||
// freshly-installed perplexed has working defaults out of the box.
|
||||
// Idempotent — never overwrites existing files.
|
||||
try {
|
||||
const result = await seedTemplatesIfMissing(this.app, this.settings.directoryTemplatesRoot);
|
||||
if (result.seeded > 0) {
|
||||
console.debug(`Perplexed Plugin: seeded ${result.seeded.toString()} template(s) (${result.reason})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Perplexed Plugin: template seeding failed:', error);
|
||||
}
|
||||
|
||||
// Initialize prompts service first
|
||||
try {
|
||||
this.promptsService = new PromptsService(this.settings.prompts);
|
||||
|
|
@ -1796,6 +1810,21 @@ class PerplexedSettingTab extends PluginSettingTab {
|
|||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Re-seed templates')
|
||||
.setDesc('Write any shipped template files that are missing from the templates root. Existing files are never overwritten — to reset a template to its shipped default, delete the file first then re-seed.')
|
||||
.addButton(btn => btn
|
||||
.setButtonText('Re-seed')
|
||||
.onClick(async () => {
|
||||
try {
|
||||
await reSeedMissingFiles(this.plugin.app, this.plugin.settings.directoryTemplatesRoot);
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
new Notice(`Re-seed failed: ${msg}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Find images for selection
|
||||
new Setting(containerEl).setName('Find images for selection').setHeading();
|
||||
containerEl.createEl('p', {
|
||||
|
|
|
|||
131
src/docs/templates/README.md
vendored
Normal file
131
src/docs/templates/README.md
vendored
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
---
|
||||
title: Perplexed — directory templates
|
||||
description: How the per-directory template system works in the perplexed plugin.
|
||||
---
|
||||
|
||||
# Perplexed — directory templates
|
||||
|
||||
This folder is the home of **directory templates** used by the perplexed plugin's `Apply directory template…` commands. Each template is one markdown file with three zones — frontmatter, a fenced configuration block (`cft`), and a heading skeleton — and it tells perplexed how to fill out a category of vault files (concepts, vocabulary, sources, etc.) using Perplexity research.
|
||||
|
||||
The plugin seeds this folder on first run with the four templates below. You can edit them, delete them, add your own — perplexed only re-seeds when the folder is missing or empty (or when you click **Re-seed templates** in settings, which only fills in templates whose filenames don't already exist).
|
||||
|
||||
## Shipped templates
|
||||
|
||||
| File | Targets paths | Use for |
|
||||
|---|---|---|
|
||||
| `concept-profile.md` | `concepts/**` | Encyclopedia-style entries on ideas, patterns, mental models. Anti-incumbent editorial stance baked in. |
|
||||
| `vocabulary-profile.md` | `Vocabulary/**` | Definitions of terms with disambiguation through an innovation-consulting lens. |
|
||||
| `source-profile.md` | `Sources/**` | Profiles of trusted sources — books, people, channels, publications, journals, reports, events. Adapts emphasis to the source's type. |
|
||||
| `toolkit-profile.md` | `Tooling/**` | Profiles of tools, products, platforms, frameworks. |
|
||||
|
||||
## How a template works
|
||||
|
||||
A template file has three zones:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: My Template
|
||||
applies-to-paths:
|
||||
- "MyDir/**"
|
||||
description: One-line description.
|
||||
---
|
||||
|
||||
# Free-form intro
|
||||
Anything before the cft block is ignored — it's documentation for you.
|
||||
|
||||
` ` `cft
|
||||
provider: perplexity
|
||||
model: sonar-pro
|
||||
return-citations: true
|
||||
return-images: true
|
||||
system: |
|
||||
System prompt for the model. Can use {{basename}}, {{frontmatter}},
|
||||
{{title}}, {{today}}, or {{frontmatter.<key>}}.
|
||||
` ` `
|
||||
|
||||
# Heading 1 — first section
|
||||
- Bullet instructions tell the model what to write here.
|
||||
|
||||
# Heading 2 — next section
|
||||
- More bullet instructions.
|
||||
|
||||
***
|
||||
|
||||
# User Notes
|
||||
|
||||
Anything below the `***` line is excluded from the request.
|
||||
```
|
||||
|
||||
### The three zones
|
||||
|
||||
1. **Frontmatter** (top, between `---` lines) — carries `title`, `applies-to-paths` (array of glob patterns), and an optional `description`. The plugin uses `applies-to-paths` to match a template to a target file.
|
||||
2. **`cft` block** (a code fence with language `cft`) — YAML config: `provider`, `model`, `search-recency`, `return-citations`, `return-images`, plus a multi-line `system:` prompt. Anything above the `cft` block is treated as documentation and dropped from the request.
|
||||
3. **Heading skeleton** (everything between the `cft` block's closing fence and the first `***`) — the user prompt. This is the markdown structure the model fills in. Bullets under each heading are *instructions to the model*, not literal output.
|
||||
|
||||
The `***` divider terminates the user prompt. Anything below it (the User Notes zone) is for your own scratch work and never reaches the model.
|
||||
|
||||
### Interpolation tokens
|
||||
|
||||
These tokens get replaced before the request is sent:
|
||||
|
||||
| Token | Resolves to |
|
||||
|---|---|
|
||||
| `{{basename}}` | The target file's filename without extension. **Use this for the H1**, never the frontmatter `title`. |
|
||||
| `{{title}}` | The frontmatter `title` field, or the basename if missing. |
|
||||
| `{{today}}` | Today's date in `YYYY-MM-DD` form. |
|
||||
| `{{frontmatter}}` | The whole frontmatter (whitelisted keys only) as YAML. Useful to give the model existing context. |
|
||||
| `{{frontmatter.<key>}}` or `{{<key>}}` | A specific frontmatter value (string, number, or array joined by commas). |
|
||||
|
||||
The frontmatter whitelist is configurable in plugin settings (default: `title, og_description, tags, og_image`). Only whitelisted keys are interpolated into prompts.
|
||||
|
||||
## Invoking a template
|
||||
|
||||
Three commands in the Obsidian command palette:
|
||||
|
||||
- **Apply directory template to current file** — picks the template that matches the active file's path via `applies-to-paths`, fills the body, stamps `cf_last_run` and `cf_last_run_model` to frontmatter.
|
||||
- **Apply directory template to folder** — runs the matched template across every markdown file under a chosen folder. Stops on Cancel.
|
||||
- **Stop directory template batch** — stops a running folder batch.
|
||||
|
||||
Modes:
|
||||
|
||||
- **Fill** — runs when the target file's body is empty or whitespace-only. The template fills the empty body.
|
||||
- **Append** — runs when the target file already has content. The template's output is appended after the existing body and before a new sources footer.
|
||||
|
||||
## Image markers and fallback
|
||||
|
||||
When a template sets `return-images: true`, the runtime auto-appends an instruction telling the model to insert `[IMAGE N: <description>]` markers in its prose where images would help. After streaming completes:
|
||||
|
||||
1. The runtime swaps each `[IMAGE N: …]` for `` using Perplexity's returned `images` array.
|
||||
2. If the model didn't emit any markers (or if the regex misses) but Perplexity *did* return images, they're appended as a `# Images` section before the sources footer.
|
||||
3. If Perplexity returned no images at all, you'll see a console warning explaining that the model/query combination didn't yield images. The image-placeholder bullet in the template body remains a manual fallback (run **Find images for selection** on the section).
|
||||
|
||||
Image quality is best with `sonar-pro`. `sonar-deep-research` does not reliably return images.
|
||||
|
||||
## Citation behavior
|
||||
|
||||
Templates auto-prepend a system directive that tells the model to inline numeric `[N]` citation markers tied 1:1 to Perplexity's returned search results. After streaming, the runtime appends a `# Sources` footer using the canonical Lossless `[N]: [Title](URL)` format. cite-wide's `Convert All Citations to Hex Format` command will swap the numeric markers for hex IDs.
|
||||
|
||||
## Frontmatter stamps
|
||||
|
||||
Every successful run stamps:
|
||||
|
||||
- `cf_last_run` — ISO timestamp of the run.
|
||||
- `cf_last_run_model` — `Provider model` label (e.g., `Perplexity sonar-pro`).
|
||||
|
||||
Books additionally get `google_books_url` stamped if the model surfaces a Google Books URL in body content and the field isn't already in frontmatter.
|
||||
|
||||
## Writing your own template
|
||||
|
||||
Copy any shipped template, change:
|
||||
|
||||
- The frontmatter `title`, `description`, and `applies-to-paths`.
|
||||
- The `cft` block's `system:` prompt to your domain.
|
||||
- The heading skeleton to the structure you want the model to fill.
|
||||
|
||||
Save it as `<your-name>-profile.md` in this folder. The plugin picks it up on the next palette invocation — no reload needed.
|
||||
|
||||
`applies-to-paths` accepts an array of glob patterns. `**` matches any depth. The first template whose patterns match the target file's path wins; specificity isn't ranked, so don't write overlapping patterns across templates.
|
||||
|
||||
## Re-seeding
|
||||
|
||||
The plugin only auto-seeds when this folder is missing or empty. To pull in a fresh shipped template (e.g., after a plugin update added one), open plugin settings → **Directory templates** → click **Re-seed templates**. That command only writes files whose filenames don't already exist; your edits to existing templates are never overwritten. To reset a template to its shipped default, delete the file first, then click Re-seed.
|
||||
111
src/services/templateSeederService.ts
Normal file
111
src/services/templateSeederService.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import type { App } from 'obsidian';
|
||||
import { Notice, normalizePath, TFolder } from 'obsidian';
|
||||
|
||||
import readmeContent from '../docs/templates/README.md';
|
||||
import conceptProfile from '../docs/templates/concept-profile.md';
|
||||
import vocabularyProfile from '../docs/templates/vocabulary-profile.md';
|
||||
import sourceProfile from '../docs/templates/source-profile.md';
|
||||
import toolkitProfile from '../docs/templates/toolkit-profile.md';
|
||||
|
||||
interface SeedFile {
|
||||
name: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const SEED_FILES: SeedFile[] = [
|
||||
{ name: 'README.md', content: readmeContent },
|
||||
{ name: 'concept-profile.md', content: conceptProfile },
|
||||
{ name: 'vocabulary-profile.md', content: vocabularyProfile },
|
||||
{ name: 'source-profile.md', content: sourceProfile },
|
||||
{ name: 'toolkit-profile.md', content: toolkitProfile },
|
||||
];
|
||||
|
||||
async function ensureFolder(app: App, folderPath: string): Promise<void> {
|
||||
const normalized = normalizePath(folderPath);
|
||||
const existing = app.vault.getAbstractFileByPath(normalized);
|
||||
if (existing) return;
|
||||
// Walk parents and create each missing segment so a nested templatesRoot
|
||||
// like `zz-cf-lib/templates` works on a fresh vault.
|
||||
const segments = normalized.split('/').filter(s => s.length > 0);
|
||||
let cursor = '';
|
||||
for (const seg of segments) {
|
||||
cursor = cursor ? `${cursor}/${seg}` : seg;
|
||||
if (!app.vault.getAbstractFileByPath(cursor)) {
|
||||
await app.vault.createFolder(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function folderHasMarkdown(app: App, folderPath: string): boolean {
|
||||
const normalized = normalizePath(folderPath);
|
||||
const folder = app.vault.getAbstractFileByPath(normalized);
|
||||
if (!(folder instanceof TFolder)) return false;
|
||||
return folder.children.some(child => child.path.endsWith('.md'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed shipped templates into the configured templates root if the folder is
|
||||
* missing or contains no markdown. Idempotent — never overwrites an existing
|
||||
* file. Designed for first-run experience: a fresh perplexed install creates
|
||||
* `zz-cf-lib/templates/` populated with the four shipped templates and a
|
||||
* README so the user has working templates without copy/paste.
|
||||
*/
|
||||
export async function seedTemplatesIfMissing(
|
||||
app: App,
|
||||
templatesRoot: string,
|
||||
options: { quiet?: boolean } = {},
|
||||
): Promise<{ seeded: number; reason: 'missing' | 'empty' | 'skipped' }> {
|
||||
const quiet = options.quiet === true;
|
||||
const normalized = normalizePath(templatesRoot);
|
||||
const existing = app.vault.getAbstractFileByPath(normalized);
|
||||
|
||||
if (existing && folderHasMarkdown(app, normalized)) {
|
||||
return { seeded: 0, reason: 'skipped' };
|
||||
}
|
||||
|
||||
const reason: 'missing' | 'empty' = existing ? 'empty' : 'missing';
|
||||
await ensureFolder(app, normalized);
|
||||
|
||||
let seeded = 0;
|
||||
for (const file of SEED_FILES) {
|
||||
const path = `${normalized}/${file.name}`;
|
||||
if (app.vault.getAbstractFileByPath(path)) continue;
|
||||
await app.vault.create(path, file.content);
|
||||
seeded++;
|
||||
}
|
||||
|
||||
if (!quiet && seeded > 0) {
|
||||
new Notice(`Perplexed: seeded ${seeded.toString()} template${seeded === 1 ? '' : 's'} at ${normalized}`);
|
||||
}
|
||||
return { seeded, reason };
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-seed: write every shipped file whose filename doesn't already exist.
|
||||
* For users who want to pull in a newly-added shipped template after a plugin
|
||||
* update without losing edits to existing templates. Bound to the "Re-seed
|
||||
* templates" settings button.
|
||||
*/
|
||||
export async function reSeedMissingFiles(
|
||||
app: App,
|
||||
templatesRoot: string,
|
||||
): Promise<{ seeded: number; skipped: number }> {
|
||||
const normalized = normalizePath(templatesRoot);
|
||||
await ensureFolder(app, normalized);
|
||||
|
||||
let seeded = 0;
|
||||
let skipped = 0;
|
||||
for (const file of SEED_FILES) {
|
||||
const path = `${normalized}/${file.name}`;
|
||||
if (app.vault.getAbstractFileByPath(path)) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
await app.vault.create(path, file.content);
|
||||
seeded++;
|
||||
}
|
||||
new Notice(
|
||||
`Perplexed: seeded ${seeded.toString()}, skipped ${skipped.toString()} (already present) at ${normalized}`,
|
||||
);
|
||||
return { seeded, skipped };
|
||||
}
|
||||
4
src/types/markdown.d.ts
vendored
Normal file
4
src/types/markdown.d.ts
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
declare module '*.md' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
Loading…
Reference in a new issue