Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
# Runtime state and local databases
.refiner/
**/.refiner/
.claude/worktrees/
**/.claude/worktrees/
.gemini/memory.json
**/.gemini/memory.json
**/.gemini/blackboard.json
Expand All @@ -19,6 +21,10 @@
.env
.env.*
!.env.example
.gemini-refiner.json
.universal-refiner.json
**/.gemini-refiner.json
**/.universal-refiner.json

# Editor and OS artifacts
.vscode/
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ That distinction matters because this repo is about credible system direction, n

```mermaid
flowchart LR
CLI["AI CLI\n(Claude / Cursor)"] -->|"stdio"| PI["PromptImprover\n(gemini-prompt-refiner)"]
CLI["AI CLI\n(Claude / Cursor)"] -->|"stdio"| PI["PromptImprover\n(universal-refiner)"]
subgraph internal["PromptImprover Engine"]
RAG["RAG Snippets\n(FlexSearch)"]
Memory["SQLite Memory\n(LocalBrain)"]
Expand Down Expand Up @@ -77,15 +77,15 @@ cd Promptimprover
./build_and_install.sh
```

Both installers perform a deterministic dependency install, run the full test suite, build the package, install it globally, and verify the `gemini-prompt-refiner` command. Add that command to your MCP client configuration. See the [Setup Guide](https://github.com/Coding-Autopilot-System/Promptimprover/wiki/Setup-Guide) for full configuration instructions.
Both installers perform a deterministic dependency install, run the full test suite, build the package, install it globally, and verify the `universal-refiner` command. Add that command to your MCP client configuration. See the [Setup Guide](https://github.com/Coding-Autopilot-System/Promptimprover/wiki/Setup-Guide) for full configuration instructions.

For optional automatic pre-prompt linting and post-execution recording, see the [cross-CLI automation guide](./docs/cross-cli-automation.md). Claude Code and Gemini CLI expose the required lifecycle hooks. Codex currently requires MCP-first instructions or explicit helper invocation because its hook lifecycle does not transparently intercept each prompt.

## Local Semantic Model

PromptImprover uses a local OpenAI-compatible endpoint before optional MCP sampling. The safe defaults target `http://localhost:9000/v1`, use `gemma3:12b` first, and fall back to `gemma3:1b`. If neither local model nor MCP sampling is available, rule-based refinement continues without semantic output.

Override the defaults per repository with `.gemini-refiner.json`:
Override the defaults per repository with `.universal-refiner.json`:

```json
{
Expand Down
4 changes: 2 additions & 2 deletions build_and_install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ try {
npm install --global . --no-fund

$package = Get-Content .\package.json -Raw | ConvertFrom-Json
$command = Get-Command gemini-prompt-refiner -ErrorAction Stop
$command = Get-Command universal-refiner -ErrorAction Stop
Write-Host "Prompt Refiner v$($package.version) installed: $($command.Source)" -ForegroundColor Green
}
finally {
Pop-Location
}
}
4 changes: 2 additions & 2 deletions build_and_install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ npm test
npm run build
npm install --global . --no-fund

command -v gemini-prompt-refiner >/dev/null 2>&1
command -v universal-refiner >/dev/null 2>&1
VERSION=$(node -p "require('./package.json').version")
printf 'Prompt Refiner v%s installed: %s\n' "$VERSION" "$(command -v gemini-prompt-refiner)"
printf 'Prompt Refiner v%s installed: %s\n' "$VERSION" "$(command -v universal-refiner)"
2 changes: 1 addition & 1 deletion docs/cross-cli-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ PromptImprover ships fail-open pre-prompt and post-execution helpers:
- `promptimprover-hook-pre` makes one latency-safe rule-based `lint_prompt` call, creates a trackable prompt ID, and injects advisory context. Interactive MCP linting continues to use semantic providers by default.
- `promptimprover-hook-post` records privacy-safe completion metadata with `record_agent_output`.

Both commands read hook JSON from stdin, write JSON only to stdout, report failures to stderr, and always allow the client to continue. They start the same built MCP server used by `gemini-prompt-refiner`. Set `PROMPTIMPROVER_SERVER_PATH` only when testing a nonstandard build.
Both commands read hook JSON from stdin, write JSON only to stdout, report failures to stderr, and always allow the client to continue. They start the same built MCP server used by `universal-refiner`. Set `PROMPTIMPROVER_SERVER_PATH` only when testing a nonstandard build.

The helpers store only prompt ID, client name, and creation time in the OS temporary directory. They do not persist prompt or response bodies. Completion records contain output length rather than response text.

Expand Down
2 changes: 1 addition & 1 deletion docs/operator-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ npm.cmd run acceptance:package-runtime
Expected result:

```text
Package runtime smoke passed: installed gemini-prompt-refiner-8.0.0 and served /api/health on <port>.
Package runtime smoke passed: installed universal-refiner-8.0.0 and served /api/health on <port>.
```

This catches missing production dependencies that are hidden by the local workspace.
Expand Down
2 changes: 1 addition & 1 deletion docs/portfolio-proof.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This repo is best read as an enterprise-oriented MCP and prompt-governance proto

## Present-State Evidence

- `universal-refiner/package.json` defines the active package as `gemini-prompt-refiner` and describes it as cross-CLI prompt refinement using an MCP server.
- `universal-refiner/package.json` defines the active package as `universal-refiner` and describes it as cross-CLI prompt refinement using an MCP server.
- `universal-refiner/tests/` contains targeted Vitest coverage for detectors, history, lessons, memory, snippets, predictive refinement, timeline behavior, and server behavior.
- `build_and_install.ps1` installs the `universal-refiner` package globally as `prompt-refiner`, showing the intended operator entry point.

Expand Down
2 changes: 1 addition & 1 deletion universal-refiner/.gemini/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"mcpServers": {
"prompt-refiner": {
"command": "gemini-prompt-refiner",
"command": "universal-refiner",
"args": []
}
}
Expand Down
18 changes: 18 additions & 0 deletions universal-refiner/.universal-refiner.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"_comment_usage": "Copy this file to .universal-refiner.json in the universal-refiner directory. ConfigManager reads .universal-refiner.json at server startup; restart the MCP server after changes.",
"_comment_fields": {
"semantic.localEnabled": "true = LocalOpenAiProvider is tried first. false = skip local model entirely.",
"semantic.baseUrl": "OpenAI-compatible /v1 base URL. Ollama default: http://localhost:11434/v1. LM Studio default: http://localhost:1234/v1.",
"semantic.models": "Model names to try in order. First reachable model wins. Use exact Ollama model tags.",
"semantic.mcpSamplingEnabled": "true = fall back to MCP sampling if local model fails or is unreachable.",
"semantic.allowNonLoopback": "Set to true only if baseUrl is a non-loopback host. Leave false for localhost."
},
"semantic": {
"localEnabled": true,
"baseUrl": "http://localhost:11434/v1",
"models": ["gemma3:12b", "gemma3"],
"mcpSamplingEnabled": true,
"timeoutMs": 120000,
"temperature": 0.2
}
}
111 changes: 111 additions & 0 deletions universal-refiner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Universal Refiner

MCP server for prompt governance. Provides the `refine_prompt` tool that enriches
prompts with project mandates, agentic context, and semantic refinement via a local
model or MCP sampling fallback.

## Quick Start

```powershell
# From the universal-refiner directory
npm run build
node dist/src/index.js
```

The server registers as an MCP stdio transport. Global registration is managed by
`scripts/operations/register-global.ps1`.

## Local Model Configuration

`refine_prompt` routes semantic refinement through `LocalOpenAiProvider` first (tier-0),
then falls back to `McpSamplingProvider` if the local model is unreachable or returns an error.

Configuration is read from `.universal-refiner.json` in the working directory at **server startup**.
After changing this file you must **restart the MCP server process** for the change to take effect.

### Wiring Ollama / Gemma

1. Start Ollama with the Gemma model:

```powershell
ollama pull gemma3:12b
ollama serve
```

Ollama listens at `http://localhost:11434` by default.

2. Create `.universal-refiner.json` in `universal-refiner/`:

```json
{
"semantic": {
"localEnabled": true,
"baseUrl": "http://localhost:11434/v1",
"models": ["gemma3:12b", "gemma3"],
"mcpSamplingEnabled": true
}
}
```

Copy `.universal-refiner.example.json` as a starting point.

3. Restart the MCP server.

### Config Fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `semantic.localEnabled` | boolean | `true` | Enable `LocalOpenAiProvider` as tier-0 |
| `semantic.baseUrl` | string | `http://localhost:9000/v1` | OpenAI-compatible `/v1` base URL. **Ollama default is port 11434, not 9000.** |
| `semantic.models` | string[] | `["gemma3:12b", "gemma3:1b"]` | Model names tried in order. First reachable model wins. |
| `semantic.mcpSamplingEnabled` | boolean | `true` | Fall back to MCP sampling if local model fails |
| `semantic.timeoutMs` | number | `120000` | Request timeout in milliseconds |
| `semantic.temperature` | number | `0.2` | Sampling temperature (0–2) |
| `semantic.allowNonLoopback` | boolean | `false` | Must be `true` for non-loopback base URLs (e.g., remote server). Leave `false` for localhost. |

> **Important:** The hardcoded default `baseUrl` is port 9000, not 11434. Ollama serves on
> port 11434. Without a `.universal-refiner.json` overriding `baseUrl`, `LocalOpenAiProvider`
> will silently fail to connect and fall through to MCP sampling.

### LM Studio

LM Studio exposes the same OpenAI-compatible `/v1` API. Use:

```json
{
"semantic": {
"localEnabled": true,
"baseUrl": "http://localhost:1234/v1",
"models": ["gemma-3-12b-it"]
}
}
```

### Provider Chain

When a `refine_prompt` call arrives:

1. `LocalOpenAiProvider` is tried first (if `localEnabled: true`).
- Iterates `models` in order. On model failure, moves to the next model.
- Returns `null` if all models fail (triggers fallback).
2. `McpSamplingProvider` is tried next (if `mcpSamplingEnabled: true`).
3. If both fail, `refine_prompt` returns the original prompt unchanged.

## Configuration File Reference

See `.universal-refiner.example.json` for an annotated template.

## Release Gate

```powershell
npm run release:verify
```

Runs build, 100% test coverage, MCP acceptance, semantic fallback, stress/soak, and audit checks.

## Security

- Never commit `.universal-refiner.json` if it contains sensitive values.
Add it to `.gitignore` if you customise it beyond the example defaults.
- `allowNonLoopback: false` (default) prevents the local provider from contacting
non-loopback hosts, limiting the blast radius of a misconfigured `baseUrl`.
8 changes: 4 additions & 4 deletions universal-refiner/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions universal-refiner/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "gemini-prompt-refiner",
"name": "universal-refiner",
"version": "8.0.0",
"description": "Cross-CLI prompt refinement using an MCP server.",
"main": "dist/src/index.js",
"bin": {
"gemini-prompt-refiner": "./dist/src/index.js",
"universal-refiner": "./dist/src/index.js",
"promptimprover-hook-pre": "./dist/hooks/pre-prompt.js",
"promptimprover-hook-post": "./dist/hooks/post-execution.js"
},
Expand Down
12 changes: 6 additions & 6 deletions universal-refiner/scripts/acceptance/package-runtime-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ try {
await runNpm(["install", "--global", "--prefix", prefixDir, "--no-fund", tarball]);

const bin = process.platform === "win32"
? join(prefixDir, "gemini-prompt-refiner.cmd")
: join(prefixDir, "bin", "gemini-prompt-refiner");
? join(prefixDir, "universal-refiner.cmd")
: join(prefixDir, "bin", "universal-refiner");
const packageRoot = await findInstalledPackageRoot(prefixDir);
const installedEntry = join(packageRoot, "dist", "src", "index.js");
await access(bin);
Expand Down Expand Up @@ -116,12 +116,12 @@ function runNpm(args) {
async function findInstalledPackageRoot(prefixDir) {
const candidates = process.platform === "win32"
? [
join(prefixDir, "node_modules", "gemini-prompt-refiner"),
join(prefixDir, "lib", "node_modules", "gemini-prompt-refiner"),
join(prefixDir, "node_modules", "universal-refiner"),
join(prefixDir, "lib", "node_modules", "universal-refiner"),
]
: [
join(prefixDir, "lib", "node_modules", "gemini-prompt-refiner"),
join(prefixDir, "node_modules", "gemini-prompt-refiner"),
join(prefixDir, "lib", "node_modules", "universal-refiner"),
join(prefixDir, "node_modules", "universal-refiner"),
];

for (const candidate of candidates) {
Expand Down
27 changes: 25 additions & 2 deletions universal-refiner/src/core/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export interface SemanticConfig {
}

export class ConfigManager {
private static CONFIG_FILE = ".gemini-refiner.json";
private static CONFIG_FILE = ".universal-refiner.json";
private static LEGACY_CONFIG_FILE = ".gemini-refiner.json";
private static DEFAULT_SEMANTIC_CONFIG: SemanticConfig = {
localEnabled: true,
mcpSamplingEnabled: true,
Expand All @@ -31,7 +32,7 @@ export class ConfigManager {
};

static loadConfig(rootPath: string = "."): RefinerConfig {
const configPath = path.join(rootPath, this.CONFIG_FILE);
const configPath = this.resolveConfigPath(rootPath);
if (!fs.existsSync(configPath)) {
return {};
}
Expand All @@ -45,6 +46,28 @@ export class ConfigManager {
}
}

private static resolveConfigPath(rootPath: string): string {
const configPath = path.join(rootPath, this.CONFIG_FILE);
if (fs.existsSync(configPath)) {
return configPath;
}

const legacyPath = path.join(rootPath, this.LEGACY_CONFIG_FILE);
if (fs.existsSync(legacyPath)) {
console.warn(`${this.LEGACY_CONFIG_FILE} is deprecated; rename it to ${this.CONFIG_FILE}.`);
return legacyPath;
}

return configPath;
}

static mergeConfig(rootPath: string = ".", updates: Partial<RefinerConfig>): void {
const configPath = path.join(rootPath, this.CONFIG_FILE);
const current = this.loadConfig(rootPath);
const merged = { ...current, ...updates };
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), "utf-8");
}

static getSemanticConfig(rootPath: string = "."): SemanticConfig {
const semantic = this.loadConfig(rootPath).semantic || {};
const defaults = this.DEFAULT_SEMANTIC_CONFIG;
Expand Down
1 change: 1 addition & 0 deletions universal-refiner/src/core/semantic-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export class LocalOpenAiProvider implements SemanticProvider {
stream: false,
temperature: this.options.temperature,
max_tokens: request.maxTokens,
keep_alive: -1,
}),
signal: AbortSignal.timeout(this.options.timeoutMs),
});
Expand Down
14 changes: 8 additions & 6 deletions universal-refiner/src/history/timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface TimelineEntry {
timestamp: string;
summary: string;
author?: string;
event_type?: string;
severity?: string;
details?: any;
}

Expand Down Expand Up @@ -35,12 +37,12 @@ export class TimelineProvider {

// Filter out prompt_recorded events because we already have the prompt record itself
const events = db.prepare(`
SELECT 'log' as type, id, timestamp, summary, event_type as author, details_json as details
FROM events
WHERE event_type NOT IN ('prompt_recorded', 'prompt_processed')
ORDER BY timestamp DESC
LIMIT ?
`).all(limit);
SELECT 'log' as type, id, timestamp, summary, event_type as author, event_type, severity, details_json as details
FROM events
WHERE event_type NOT IN ('prompt_recorded', 'prompt_processed')
ORDER BY timestamp DESC
LIMIT ?
`).all(limit);

const unified: TimelineEntry[] = [
...prompts.map((p: any) => ({ ...p, details: { intent: p.details } })),
Expand Down
Loading