Never forget a good prompt.
Instead of retyping a good prompt from memory or digging it out of a transcript, compose it from a template with typed fields — then hand it to your agent.
Built by Ansicht.
Struktek is published on the Open VSX Registry, so it installs in VS Code, VSCodium, Cursor, Windsurf, and anything else that uses Open VSX.
A template is a markdown file. Three constructs in the body, nothing else:
| Construct | Meaning |
|---|---|
{{ name }} |
a placeholder, type text |
{{ name: type "description" }} |
typed; the description is optional |
[ ... ] |
a segment that vanishes when every placeholder inside it is blank |
---
name: code-review
description: Review a file for a specific class of problem
---
Review {{ target: file "path relative to repo root" }} for {{ focus: choice[correctness, perf, security] }}.
[Pay particular attention to {{ emphasis }}.]
{{ format: output-format = json-strict }}Filled in as target=src/auth.ts, focus=security, that renders:
Review src/auth.ts for security.
Return JSON only — no prose, no code fence, no commentary.
The optional line dropped out cleanly, and format used its pinned default.
Built in: text (the default), block (multi-line), number, file,
and choice[a, b, c].
Your own — a directory is a type, and the files inside it are the instances:
.struktek/
templates/
code-review.md
blocks/
output-format/ <- the type
json-strict.md <- the instances
markdown-table.md
prose.md
depth/
quick.md
thorough.md
A choice yields a word. A block type yields the whole instance file. That
difference is invisible at the call site by design — {{ format }} reads the
same either way.
Drop in blocks/output-format/csv.md and csv becomes valid in every
template with an output-format field. Improve a block once and everything
using it improves at the same time. That is the part a flat list of snippets
cannot do.
- Type once. A field can appear many times; annotate whichever occurrence reads best. Two occurrences with different types is an error, not last-wins.
= valuepins a default. Pre-filled, still overridable — which is why there is no separate "always include this block" syntax.- Frontmatter is optional and wins. Reach for it when a field needs a long description or a default, and leave the body clean.
- Brackets with no placeholder inside are prose.
[see notes]stays exactly as written. Escape a real one as\[if you need to. - Last-used values come back. The second time you use a template, each field is pre-filled with whatever you picked last time.
- Blocks can carry a header too.
title,description,tagsandnotein frontmatter describe a block in the sidebar. Everything below the fence is what actually lands in the prompt — the header never renders.
Templates live in .struktek/ in your workspace. They can also live in
~/.struktek/, which is visible from every workspace — the same layout,
one level up:
~/.struktek/ <- global, everywhere
templates/commit-message.md
blocks/depth/forensic.md
my-project/.struktek/ <- this project only
templates/code-review.md
blocks/depth/forensic.md <- wins; the global one is shown as overridden
The two are merged into one library. The workspace wins a name collision,
the way git config and VS Code settings resolve — so a project can override a
global template without renaming anything, and the copy it displaced is still
listed, marked as overridden, so you can see why.
Block types union rather than collide: a blocks/depth/ folder in your global
library makes {{ how: depth }} a valid field in every project you open.
Moving between them is a file move. Any row in the sidebar has a globe
(make global) or a folder (make workspace-only), and the same pair is in the
palette as Struktek: Make Global / Struktek: Make Workspace-Only. A block
type moves as a unit — all of its values.
Struktek asks first when the move is not just a move: when the destination name is taken, when demoting something you might rely on elsewhere, and — the one worth the dialog — when promoting a template whose block types exist only in this workspace. That last one reads fine here and reports an unknown type in every other project, so it offers to bring the blocks along.
Templates in the global library work with no folder open at all, and the offline MCP bridge reads both roots, so a global template is there in a bare terminal too. Use counts and history stay per-workspace: how often you reach for a template here is the useful question.
Set struktek.globalLibrary.path to keep it somewhere else, or
struktek.globalLibrary.enabled to false to use the workspace library alone.
The search box narrows templates and blocks as you type, matching names, descriptions, notes and tags. Two buttons sit beside it, each opening a menu with submenus — menus rather than panels that unfold, so choosing never moves the list underneath you.
Filter holds one section per dimension. In the sidebar that is Tags; tick as many as you like and the funnel fills in while a filter is on.
Sort holds one submenu per field, each with both directions:
| Relevance | Most used · Least used |
| Name | Alphabetical · Reverse alphabetical |
| Date | Newest · Oldest |
The field the list is currently ordered by carries the name of that order beside it, so how a list is sorted is readable without opening anything.
Most used is the default and counts prompts composed from a template in this workspace. You will not find that number anywhere in the UI, and that is on purpose: a count beside every row is something you read past on each pass, while the order it produces needs no reading at all. Blocks carry no such count, so they stay alphabetical whichever sort is chosen.
Overridden rows sort last under every option — a struck-through row should never sit above one you can actually compose.
The history screen has the same row over prompts instead of templates: Filter holds Templates and Tags, and Sort holds Date and Template. Ordered by template, the runs inside one name stay newest-first — the name is the key you chose, and recency is still what you want to read first inside it.
Struktek: Open Panel opens beside what you are looking at rather than as
another tab on top of it — the panel is something you compose from, so the
one thing it must not do is hide the file you opened it for. Two screens.
Compose is the fields on the left and the prompt as it will be sent on the right, split by a divider you can drag - how much room a form needs depends on the template, and how much a preview needs depends on the prompt. Optional fields fold away so a template reads as long as it actually is. The template name is also the switcher.
History is every prompt you have composed, newest first. The search box matches the prompt text, the template, its tags and the values behind it, and the funnel and sort button beside it work as they do in the sidebar.
Click a run to open it in full: the whole prompt, scrolling on its own, the value each field was filled with, and every action the card carries. What it was made from is a set of links — the template opens in the composer, a block opens its file — which is the one thing a prompt cannot tell you by reading it.
Copy takes the text. Create variant reopens the composer with the values that
run actually used. The bin removes that one run, and asks nothing: it is a line
in a log and it is in front of you when you press it, where clearing a
template's history or all of it throws away runs you cannot see and still asks
first. Runs are kept in .struktek/.runtime/history.jsonl, which is
git-ignored.
There is no library screen: the sidebar is the library, and a second grid of the same templates was one surface too many.
Prompts your agent composes over MCP land there too, tagged mcp — with one
exception. When VS Code is closed the bridge reads templates straight off disk
and deliberately writes nothing, so there is only ever one writer per file;
prompts composed in a bare terminal are not recorded.
Keep more or fewer with struktek.history.limit, or 0 to keep none.
Most good prompts are never written as templates. They are typed once into an agent, they work, and they are gone. Struktek can read those back.
Turn on struktek.harvest.enabled and History also shows what you have typed
at Claude Code and Codex, in the same feed, newest first. History
offers this while it is off, above the feed; dismissing the offer stops it for
good on that machine. A Source
filter separates the two kinds — only the raw material, or only what I
composed — and the search box spans both.
It is read-only. Struktek never opens any of these files for writing. Off by default, because they hold every prompt you have typed at an agent in every project on the machine, pasted content included, and that is not something to surface without being asked.
Each agent keeps two records, and struktek reads all four:
~/.claude/projects/ |
every Claude Code session, terminal and editor |
~/.claude/history.jsonl |
terminal sessions only, reaching further back |
~/.codex/sessions/ |
every Codex session, and where each one ran |
~/.codex/history.jsonl |
Codex submissions, up to whenever it last wrote one |
The per-session directories are the real records. Both flat logs turn out to be
partial, in different ways and without saying so: Claude Code writes
history.jsonl from the terminal CLI alone, so working through the VS Code
extension leaves it empty; Codex's simply stopped being written at some point
while sessions carried on. Either way the symptom is the same — a feed that
looks like you have not used that agent much.
The flat logs are still read, because they reach back past whatever sessions have been kept on disk. Where they overlap with the session records, a prompt recorded twice collapses into one row rather than being counted as typed twice.
A Codex session record also says which directory it ran in, which its flat log never did. That is what lets a workspace-scoped feed leave out Codex prompts belonging to other projects instead of showing all of them everywhere.
Not everything wearing the user's name in those files was typed by one. Codex
addresses the model as the user for IDE context, environment blocks, AGENTS.md
and pasted attachments; Claude Code records tool results the same way. Those are
filtered out by shape, and the filters are deliberately narrow — one of the best
prompts in the corpus this was built against opens #code #map #discovery, and a
blanket "starts with a hash" rule would have eaten it.
Most of a log is not prompts. On one real Claude Code log of 7,063
submissions, 1,645 were bare slash commands and 5,126 were under eighty
characters — yes, continue, /model, a filename. So slash commands and
one-word replies are dropped, anything under struktek.harvest.minLength
is dropped, and the same text typed twice collapses into one row that says
typed 6×. Roughly 2,000 of those 7,063 survive. Lower the floor if that is
too harsh; nothing is ever deleted from the agent's own file.
Each card carries Templatize, which writes a template from the prompt and
opens it. Two things happen on the way in. Brackets and braces are escaped, so
nothing you typed disappears — an unescaped [phrase] is an optional segment
with no field in it, and would silently collapse to nothing. And Claude Code's
[Pasted text #1 +85 lines] markers become real fields, because a paste is the
one span in a prompt the author already marked as the part that changes.
Everything else is left exactly as typed: marking up the rest is your call.
Beside it, Copy, Save as a block value for a prompt that is really a reusable part, and Hide for one you never want to see again — with an Undo, and no bin, because there is nothing here struktek is entitled to delete.
struktek.harvest.scope decides how wide the net is. workspace, the default,
shows what you typed while working in this folder; all shows the machine. A
prompt whose directory cannot be established at all is shown under either,
since hiding what cannot be placed would hide it everywhere — but after the
session lookups that is now a rare case rather than the normal one for Codex.
Reading is incremental and cached per file. The transcripts run to hundreds of megabytes across a year, so a repaint re-reads only the session file that actually moved — usually the one you are typing into — and reuses the parse of the hundreds that did not.
Open a template and Struktek works on it directly: unknown types, conflicting
annotations and unmatched brackets appear in the Problems panel on the right
character, placeholders are highlighted, hovering a field says what it resolves
to, and typing : or = completes the built-in types, your own block types,
and the values a block type has.
Templates stay ordinary Markdown - no special extension. The editor features
apply to files under your library folder, wherever struktek.libraryPath
points.
| Command | |
|---|---|
Struktek: Compose Prompt |
Ctrl+Shift+K — pick a template, fill it, send it |
Struktek: New Template |
create a blank template and open it |
Struktek: New Block |
add a value to a block type, or start a new type |
Struktek: Open Panel |
the composer, and the history of every prompt you have produced |
Struktek: Open Template Library |
browse and edit what you have |
Struktek: Make Global |
move a template or block into ~/.struktek, for every workspace |
Struktek: Make Workspace-Only |
move it back into this project |
Struktek: Reveal Global Library |
open ~/.struktek in your file manager |
Struktek: Configure MCP for Agent |
wire your templates into Claude Code or Codex |
Struktek: Report an Issue |
open a bug report or a feature request, prefilled |
Composing ends with a choice of Send to Chat (prefills the chat box without submitting), Copy to Clipboard, Insert at Cursor, or Open in Editor.
Run Struktek: Configure MCP for Agent and your templates become available to
your agent three ways at once:
- As slash commands —
/mcp__struktek__code-review, with each field an argument. This is MCP'spromptsprimitive, which a template maps onto exactly. - As tools —
struktek_list_templatesandstruktek_compose, so the model can pick a template itself. Necessary because MCP prompts are invisible to the model: you can invoke one, but the agent cannot see it to choose. This is what lets an agent compose a good prompt for its own subagents. - As resources —
struktek://template/{name}andstruktek://block/{type}/{instance}, returning a file as written. Listing a template reports its fields; reading the resource is the only way to see its wording, which is what an agent needs before it edits one.
On connect the server also sends instructions — that there is a library worth checking before writing a prompt from scratch, that a block type is shared vocabulary, and that composing reports what it left unfilled. Tool descriptions are read after deciding to look; instructions arrive before.
A server icon in the status bar says whether the server is up and how many agents are attached; it turns amber if the server could not start, which is the one failure you would otherwise only find in the output channel. Click it to configure an agent, copy the URL, read the log, or restart. Nothing appears when MCP is switched off or the workspace cannot host it.
The generated config launches npx -y @struktek/mcp-bridge — no port, no token,
no absolute path — so it is static, committable, and survives restarts.
When VS Code is running the bridge proxies to it, keeping one writer for usage stats and last-used values. When it is not, templates are read straight off disk — both libraries, merged the same way — so your slash commands still work in a bare terminal.
struktek_save_template and struktek_save_block take a scope, offered only
when there are two libraries to choose between. It defaults to the workspace:
an agent should not make something global for every project you open unless you
asked for it.
npm install
npm run build # or: npm run watch
npm test # vitest — unit + integration
npm run lint # tsc --noEmit
npm run package:bridge # build the publishable npm bridge| Setting | Default | |
|---|---|---|
struktek.libraryPath |
.struktek |
where this workspace's templates and blocks live |
struktek.globalLibrary.enabled |
true |
also load a library visible from every workspace |
struktek.globalLibrary.path |
~/.struktek |
where that library lives |
struktek.history.limit |
500 |
how many composed prompts to keep per workspace |
struktek.harvest.enabled |
false |
read Claude Code and Codex prompt logs into History |
struktek.harvest.scope |
workspace |
show this folder's prompts, or the whole machine's |
struktek.harvest.minLength |
80 |
ignore submissions shorter than this |
struktek.harvest.sources |
[] |
log files to read instead of the defaults |
struktek.mcp.enabled |
true |
run the MCP server so agents can reach your templates |
struktek.logLevel |
info |
verbosity of the Struktek output channel |
struktek.debug.styleTokens |
false |
debug: add a Tokens screen listing every theme variable the views draw with |
F5 launches an Extension Development Host on test-fixtures/workspace.
npm run install:local packages the .vsix and installs it into every editor
CLI it finds — VS Code, VSCodium, Insiders, Cursor, Windsurf.
The parser lives in src/core/ and imports nothing — no vscode,
no node:*. Filesystem and YAML access are injected, so the extension host, the
MCP bridge, and any future webview all run the same code.
Pre-release (0.3.0) and shaped by daily use, so expect the format to move
before 1.0.
The source is public to read, review, and learn from. It is not open source: struktek is proprietary, all rights reserved — see LICENSE.txt. Your templates and blocks are entirely yours; the licence claims nothing over anything you author with it.
Bug reports and ideas are welcome in issues — or from the sidebar's title bar, which asks whether you are reporting a bug or requesting a feature and opens that form with the version, editor and platform already filled in. Security issues should go through SECURITY.md rather than a public issue.
