@aexol/spectral 0.3.8 → 0.4.1

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 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
- const extFlags = ["--extension", aexolExtPath, "--extension", mcpExtPath, "--extension", obsMemPath];
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
+ }
@@ -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. No UI/command context needed in serve mode.
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.3.8",
3
+ "version": "0.4.1",
4
4
  "description": "Always-on coding agent for Aexol — branded pi wrapper with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -51,12 +51,12 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@inquirer/prompts": "^7.2.0",
54
- "@mariozechner/jiti": "^2.6.5",
55
- "@mariozechner/pi-coding-agent": "^0.70.2",
54
+ "@mariozechner/jiti": "2.6.5",
55
+ "@mariozechner/pi-coding-agent": "0.70.2",
56
56
  "better-sqlite3": "^12.9.0",
57
- "@mariozechner/pi-agent-core": "^0.70.2",
58
- "@mariozechner/pi-ai": "^0.70.2",
59
- "@mariozechner/pi-tui": "^0.70.2",
57
+ "@mariozechner/pi-agent-core": "0.70.2",
58
+ "@mariozechner/pi-ai": "0.70.2",
59
+ "@mariozechner/pi-tui": "0.70.2",
60
60
  "@modelcontextprotocol/sdk": "^1.25.1",
61
61
  "@modelcontextprotocol/ext-apps": "^1.2.2",
62
62
  "open": "^10.2.0",