@aexol/spectral 0.3.8 → 0.3.9
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/cli.js +11 -1
- package/dist/extensions/openrouter-attribution.js +19 -0
- package/dist/server/pi-bridge.js +65 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -87,6 +87,10 @@ function resolveObservationalMemoryPath() {
|
|
|
87
87
|
function resolveMcpExtensionPath() {
|
|
88
88
|
return resolve(__dirname, "mcp", "index.js");
|
|
89
89
|
}
|
|
90
|
+
/** Absolute path to the bundled openrouter-attribution extension, sitting next to this file in dist/. */
|
|
91
|
+
function resolveOpenRouterAttributionPath() {
|
|
92
|
+
return resolve(__dirname, "extensions", "openrouter-attribution.js");
|
|
93
|
+
}
|
|
90
94
|
// ---- Branded helpers ---------------------------------------------------------
|
|
91
95
|
function printVersion() {
|
|
92
96
|
process.stdout.write(`spectral ${VERSION} — ${TAGLINE}\n`);
|
|
@@ -218,7 +222,13 @@ async function main() {
|
|
|
218
222
|
process.stderr.write(`spectral: bundled observational-memory extension not found at ${obsMemPath}. This is a packaging bug.\n`);
|
|
219
223
|
process.exit(1);
|
|
220
224
|
}
|
|
221
|
-
|
|
225
|
+
// OpenRouter attribution extension – adds headers so Aexol appears in OpenRouter rankings
|
|
226
|
+
const openrouterAttrPath = resolveOpenRouterAttributionPath();
|
|
227
|
+
if (!existsSync(openrouterAttrPath)) {
|
|
228
|
+
process.stderr.write(`spectral: bundled OpenRouter attribution extension not found at ${openrouterAttrPath}. This is a packaging bug.\n`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
const extFlags = ["--extension", aexolExtPath, "--extension", mcpExtPath, "--extension", obsMemPath, "--extension", openrouterAttrPath];
|
|
222
232
|
const finalArgs = [...extFlags, ...args];
|
|
223
233
|
delegateToPi(finalArgs);
|
|
224
234
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter App Attribution extension.
|
|
3
|
+
*
|
|
4
|
+
* Adds HTTP headers required by OpenRouter's app attribution system so that
|
|
5
|
+
* Aexol / Spectral appears in OpenRouter rankings, analytics, and model
|
|
6
|
+
* "Apps" tabs. Without these headers our usage is anonymous to OpenRouter.
|
|
7
|
+
*
|
|
8
|
+
* @see https://openrouter.ai/docs/app-attribution
|
|
9
|
+
*/
|
|
10
|
+
export default function openrouterAttributionExtension(pi) {
|
|
11
|
+
pi.registerProvider("openrouter", {
|
|
12
|
+
headers: {
|
|
13
|
+
"HTTP-Referer": "https://aexol.ai",
|
|
14
|
+
"X-OpenRouter-Title": "Aexol",
|
|
15
|
+
"X-OpenRouter-Categories": "cli-agent,cloud-agent",
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
process.stderr.write("[openrouter-attribution] Registered OpenRouter app attribution headers.\n");
|
|
19
|
+
}
|
package/dist/server/pi-bridge.js
CHANGED
|
@@ -502,12 +502,20 @@ export class PiBridge {
|
|
|
502
502
|
modelRegistry: this.modelRegistry,
|
|
503
503
|
});
|
|
504
504
|
this.session = result.session;
|
|
505
|
+
// Headless UI context: forwards extension notify() calls as wire events
|
|
506
|
+
// so the browser can surface memory activity, MCP status, etc.
|
|
507
|
+
const uiContext = createHeadlessUIContext((event) => {
|
|
508
|
+
try {
|
|
509
|
+
this.opts.emit(event);
|
|
510
|
+
}
|
|
511
|
+
catch { /* best-effort */ }
|
|
512
|
+
});
|
|
505
513
|
// Emit session_start so extensions can initialize (e.g. pi-mcp-adapter
|
|
506
514
|
// connects to MCP servers, loads configs from ~/.config/mcp/mcp.json etc.).
|
|
507
515
|
// bindExtensions also fires resources_discover for dynamic skill/prompt
|
|
508
|
-
// registration.
|
|
516
|
+
// registration.
|
|
509
517
|
try {
|
|
510
|
-
await this.session.bindExtensions({});
|
|
518
|
+
await this.session.bindExtensions({ uiContext });
|
|
511
519
|
console.info("[PiBridge] session_start emitted; extensions initialized.");
|
|
512
520
|
}
|
|
513
521
|
catch (err) {
|
|
@@ -1017,3 +1025,58 @@ export class PiBridge {
|
|
|
1017
1025
|
}
|
|
1018
1026
|
}
|
|
1019
1027
|
}
|
|
1028
|
+
// ---------------------------------------------------------------------------
|
|
1029
|
+
// Headless UI context
|
|
1030
|
+
// ---------------------------------------------------------------------------
|
|
1031
|
+
/**
|
|
1032
|
+
* Detect the memory subsystem from a notification message text.
|
|
1033
|
+
* The memory extension prefixes all messages with "Observational memory:".
|
|
1034
|
+
*/
|
|
1035
|
+
function detectMemorySystem(message) {
|
|
1036
|
+
if (!message.startsWith("Observational memory:"))
|
|
1037
|
+
return undefined;
|
|
1038
|
+
const lower = message.toLowerCase();
|
|
1039
|
+
if (lower.includes("observer"))
|
|
1040
|
+
return "memory_observer";
|
|
1041
|
+
if (lower.includes("compaction") || lower.includes("compact"))
|
|
1042
|
+
return "memory_compaction";
|
|
1043
|
+
if (lower.includes("reflection"))
|
|
1044
|
+
return "memory_reflection";
|
|
1045
|
+
if (lower.includes("pruner") || lower.includes("prune"))
|
|
1046
|
+
return "memory_pruner";
|
|
1047
|
+
return "extension";
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Create a minimal ExtensionUIContext that forwards `notify()` calls as
|
|
1051
|
+
* `agent_notification` wire events. All other UI methods are no-ops —
|
|
1052
|
+
* only extensions (not pi's TUI) call into the UI context in headless mode,
|
|
1053
|
+
* and extensions that call methods other than `notify()` are expected to
|
|
1054
|
+
* guard with `ctx.hasUI` first.
|
|
1055
|
+
*/
|
|
1056
|
+
function createHeadlessUIContext(emit) {
|
|
1057
|
+
// Defer to a Proxy so we don't need to stub every method.
|
|
1058
|
+
// `notify` is the only method called by extensions in serve mode
|
|
1059
|
+
// (observational memory, MCP status bar updates).
|
|
1060
|
+
const handler = {
|
|
1061
|
+
get(_target, prop) {
|
|
1062
|
+
if (prop === "notify") {
|
|
1063
|
+
return (message, type) => {
|
|
1064
|
+
emit({
|
|
1065
|
+
type: "agent_notification",
|
|
1066
|
+
message,
|
|
1067
|
+
level: type ?? "info",
|
|
1068
|
+
system: detectMemorySystem(message),
|
|
1069
|
+
});
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
// All other methods: return a no-op function or undefined.
|
|
1073
|
+
// Methods that return Promises (select, confirm, input, custom, editor)
|
|
1074
|
+
// resolve to `undefined` — extensions are expected to guard with `ctx.hasUI`
|
|
1075
|
+
// before calling them, and none do in serve mode.
|
|
1076
|
+
// Methods that set state (setStatus, setWorkingMessage, setWidget, etc.)
|
|
1077
|
+
// are safe to no-op.
|
|
1078
|
+
return () => undefined;
|
|
1079
|
+
},
|
|
1080
|
+
};
|
|
1081
|
+
return new Proxy({}, handler);
|
|
1082
|
+
}
|