@aexol/spectral 0.7.6 → 0.7.8

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.
Files changed (57) hide show
  1. package/dist/agent/index.js +16 -140
  2. package/dist/cli.js +25 -220
  3. package/dist/extensions/spectral-vision-fallback.js +188 -0
  4. package/dist/memory/commands/status.js +5 -5
  5. package/dist/memory/commands/view.js +16 -14
  6. package/dist/memory/compaction.js +31 -3
  7. package/dist/memory/prompts.js +5 -5
  8. package/dist/memory/tools/recall-observation.js +2 -2
  9. package/dist/pi/coding-agent/config.js +0 -11
  10. package/dist/pi/coding-agent/core/agent-session.js +3 -17
  11. package/dist/pi/coding-agent/core/extensions/loader.js +0 -6
  12. package/dist/pi/coding-agent/core/extensions/runner.js +7 -1
  13. package/dist/pi/coding-agent/core/keybindings.js +129 -2
  14. package/dist/pi/coding-agent/core/settings-manager.js +20 -0
  15. package/dist/pi/coding-agent/core/tools/bash.js +17 -63
  16. package/dist/pi/coding-agent/core/tools/edit.js +4 -141
  17. package/dist/pi/coding-agent/core/tools/find.js +0 -11
  18. package/dist/pi/coding-agent/core/tools/grep.js +0 -11
  19. package/dist/pi/coding-agent/core/tools/ls.js +0 -11
  20. package/dist/pi/coding-agent/core/tools/read.js +0 -12
  21. package/dist/pi/coding-agent/core/tools/render-utils.js +1 -14
  22. package/dist/pi/coding-agent/core/tools/write.js +2 -97
  23. package/dist/pi/coding-agent/modes/interactive/components/keybinding-hints.js +1 -1
  24. package/dist/pi/coding-agent/modes/interactive/components/visual-truncate.js +6 -12
  25. package/dist/pi/coding-agent/modes/interactive/theme/theme.js +1 -2
  26. package/dist/relay/models-fetch.js +13 -1
  27. package/dist/server/pi-bridge.js +57 -4
  28. package/dist/server/session-stream.js +7 -1
  29. package/package.json +1 -1
  30. package/dist/pi/coding-agent/core/export-html/ansi-to-html.js +0 -248
  31. package/dist/pi/coding-agent/core/export-html/index.js +0 -225
  32. package/dist/pi/coding-agent/core/export-html/tool-renderer.js +0 -107
  33. package/dist/pi/tui/autocomplete.js +0 -631
  34. package/dist/pi/tui/components/box.js +0 -103
  35. package/dist/pi/tui/components/cancellable-loader.js +0 -34
  36. package/dist/pi/tui/components/editor.js +0 -1915
  37. package/dist/pi/tui/components/image.js +0 -88
  38. package/dist/pi/tui/components/input.js +0 -425
  39. package/dist/pi/tui/components/loader.js +0 -68
  40. package/dist/pi/tui/components/markdown.js +0 -633
  41. package/dist/pi/tui/components/select-list.js +0 -158
  42. package/dist/pi/tui/components/settings-list.js +0 -184
  43. package/dist/pi/tui/components/spacer.js +0 -22
  44. package/dist/pi/tui/components/text.js +0 -88
  45. package/dist/pi/tui/components/truncated-text.js +0 -50
  46. package/dist/pi/tui/editor-component.js +0 -1
  47. package/dist/pi/tui/fuzzy.js +0 -109
  48. package/dist/pi/tui/index.js +0 -31
  49. package/dist/pi/tui/keybindings.js +0 -173
  50. package/dist/pi/tui/keys.js +0 -1172
  51. package/dist/pi/tui/kill-ring.js +0 -43
  52. package/dist/pi/tui/stdin-buffer.js +0 -360
  53. package/dist/pi/tui/terminal-image.js +0 -335
  54. package/dist/pi/tui/terminal.js +0 -324
  55. package/dist/pi/tui/tui.js +0 -1076
  56. package/dist/pi/tui/undo-stack.js +0 -24
  57. package/dist/pi/tui/utils.js +0 -1016
@@ -26,7 +26,6 @@
26
26
  */
27
27
  import { agentLoop } from "../pi/agent-core/index.js";
28
28
  import { StringEnum } from "../pi/ai/index.js";
29
- import { Text } from "../pi/tui/index.js";
30
29
  import { Type } from "typebox";
31
30
  import { discoverAgents } from "./agents.js";
32
31
  // ---------------------------------------------------------------------------
@@ -235,17 +234,19 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
235
234
  agent: agentName,
236
235
  agentSource: agent.source,
237
236
  task,
238
- exitCode: 0,
237
+ exitCode: -1, // -1 = still running; 0 = success; 1 = error
239
238
  messages: [],
240
239
  stderr: "",
241
240
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
242
241
  model: model.id,
243
242
  step,
244
243
  };
244
+ // Accumulated streaming text from text_delta events (reset per assistant turn).
245
+ let streamingText = "";
245
246
  const emitUpdate = () => {
246
247
  if (onUpdate) {
247
248
  onUpdate({
248
- content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
249
+ content: [{ type: "text", text: getFinalOutput(currentResult.messages) || streamingText || "(running...)" }],
249
250
  details: makeDetails([currentResult]),
250
251
  });
251
252
  }
@@ -271,10 +272,22 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
271
272
  hadError = true;
272
273
  break;
273
274
  }
275
+ if (event.type === "message_update" && event.message?.role === "assistant") {
276
+ // Forward streaming text deltas so the UI can show the subagent's
277
+ // response as it's generated, not just the final result.
278
+ const inner = event.assistantMessageEvent;
279
+ if (inner?.type === "text_delta" && inner.delta) {
280
+ streamingText += inner.delta;
281
+ emitUpdate();
282
+ }
283
+ continue;
284
+ }
274
285
  if (event.type === "message_end" && event.message) {
275
286
  const msg = event.message;
276
287
  collectedMessages.push(msg);
277
288
  if (msg.role === "assistant") {
289
+ // Reset streaming accumulator — final text is now in messages
290
+ streamingText = "";
278
291
  currentResult.usage.turns++;
279
292
  const usage = msg.usage;
280
293
  if (usage) {
@@ -600,142 +613,5 @@ export default function subagentExtension(pi) {
600
613
  details: makeDetails("single")([]),
601
614
  };
602
615
  },
603
- // ── Rendering (headless-compatible: plain text strings, no TUI imports) ──
604
- renderCall(args, _theme, _context) {
605
- const scope = args.agentScope ?? "user";
606
- if (args.chain && args.chain.length > 0) {
607
- let text = `subagent chain (${args.chain.length} steps) [${scope}]`;
608
- for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
609
- const step = args.chain[i];
610
- const cleanTask = step.task.replace(/\{previous\}/g, "").trim();
611
- const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask;
612
- text += `\n ${i + 1}. ${step.agent} ${preview}`;
613
- }
614
- if (args.chain.length > 3)
615
- text += `\n ... +${args.chain.length - 3} more`;
616
- return new Text(text);
617
- }
618
- if (args.tasks && args.tasks.length > 0) {
619
- let text = `subagent parallel (${args.tasks.length} tasks) [${scope}]`;
620
- for (const t of args.tasks.slice(0, 3)) {
621
- const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
622
- text += `\n ${t.agent} ${preview}`;
623
- }
624
- if (args.tasks.length > 3)
625
- text += `\n ... +${args.tasks.length - 3} more`;
626
- return new Text(text);
627
- }
628
- const agentName = args.agent || "...";
629
- const preview = args.task
630
- ? args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task
631
- : "...";
632
- return new Text(`subagent ${agentName} [${scope}]\n ${preview}`);
633
- },
634
- renderResult(result, _opts, _theme, _context) {
635
- const details = result.details;
636
- if (!details || details.results.length === 0) {
637
- const text = result.content[0];
638
- return new Text(text?.type === "text" ? text.text : "(no output)");
639
- }
640
- const lines = [];
641
- if (details.mode === "single" && details.results.length === 1) {
642
- const r = details.results[0];
643
- const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
644
- const icon = isError ? "✗" : "✓";
645
- const finalOutput = getFinalOutput(r.messages);
646
- lines.push(`${icon} ${r.agent} (${r.agentSource})${isError && r.stopReason ? ` [${r.stopReason}]` : ""}`);
647
- if (isError && r.errorMessage)
648
- lines.push(`Error: ${r.errorMessage}`);
649
- lines.push("");
650
- lines.push("─── Task ───");
651
- lines.push(r.task);
652
- lines.push("");
653
- lines.push("─── Output ───");
654
- if (!finalOutput) {
655
- lines.push("(no output)");
656
- }
657
- else {
658
- lines.push(finalOutput.trim());
659
- }
660
- const usageStr = formatUsageStats(r.usage, r.model);
661
- if (usageStr) {
662
- lines.push("");
663
- lines.push(usageStr);
664
- }
665
- return new Text(lines.join("\n"));
666
- }
667
- const aggregateUsage = (results) => {
668
- const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
669
- for (const r of results) {
670
- total.input += r.usage.input;
671
- total.output += r.usage.output;
672
- total.cacheRead += r.usage.cacheRead;
673
- total.cacheWrite += r.usage.cacheWrite;
674
- total.cost += r.usage.cost;
675
- total.turns += r.usage.turns;
676
- }
677
- return total;
678
- };
679
- if (details.mode === "chain") {
680
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
681
- const icon = successCount === details.results.length ? "✓" : "✗";
682
- lines.push(`${icon} chain ${successCount}/${details.results.length} steps`);
683
- for (const r of details.results) {
684
- const rIcon = r.exitCode === 0 ? "✓" : "✗";
685
- const finalOutput = getFinalOutput(r.messages);
686
- lines.push("");
687
- lines.push(`─── Step ${r.step}: ${r.agent} ${rIcon}`);
688
- lines.push(`Task: ${r.task}`);
689
- if (finalOutput)
690
- lines.push(finalOutput.trim());
691
- else
692
- lines.push("(no output)");
693
- const stepUsage = formatUsageStats(r.usage, r.model);
694
- if (stepUsage)
695
- lines.push(stepUsage);
696
- }
697
- const usageStr = formatUsageStats(aggregateUsage(details.results));
698
- if (usageStr) {
699
- lines.push("");
700
- lines.push(`Total: ${usageStr}`);
701
- }
702
- return new Text(lines.join("\n"));
703
- }
704
- if (details.mode === "parallel") {
705
- const running = details.results.filter((r) => r.exitCode === -1).length;
706
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
707
- const failCount = details.results.filter((r) => r.exitCode > 0).length;
708
- const isRunning = running > 0;
709
- const icon = isRunning ? "⏳" : failCount > 0 ? "◐" : "✓";
710
- const status = isRunning
711
- ? `${successCount + failCount}/${details.results.length} done, ${running} running`
712
- : `${successCount}/${details.results.length} tasks`;
713
- lines.push(`${icon} parallel ${status}`);
714
- for (const r of details.results) {
715
- const rIcon = r.exitCode === -1 ? "⏳" : r.exitCode === 0 ? "✓" : "✗";
716
- const finalOutput = getFinalOutput(r.messages);
717
- lines.push("");
718
- lines.push(`─── ${r.agent} ${rIcon}`);
719
- lines.push(`Task: ${r.task}`);
720
- if (finalOutput)
721
- lines.push(finalOutput.trim());
722
- else
723
- lines.push(r.exitCode === -1 ? "(running...)" : "(no output)");
724
- const taskUsage = formatUsageStats(r.usage, r.model);
725
- if (taskUsage)
726
- lines.push(taskUsage);
727
- }
728
- if (!isRunning) {
729
- const usageStr = formatUsageStats(aggregateUsage(details.results));
730
- if (usageStr) {
731
- lines.push("");
732
- lines.push(`Total: ${usageStr}`);
733
- }
734
- }
735
- return new Text(lines.join("\n"));
736
- }
737
- const text = result.content[0];
738
- return new Text(text?.type === "text" ? text.text : "(no output)");
739
- },
740
616
  });
741
617
  }
package/dist/cli.js CHANGED
@@ -2,120 +2,26 @@
2
2
  /**
3
3
  * @aexol/spectral — Coding agent that never sleeps
4
4
  *
5
- * Thin branding wrapper around @mariozechner/pi-coding-agent (pi).
6
- *
7
- * Delegation strategy: SUBPROCESS spawn of pi's bin.
8
- *
9
- * Why subprocess and not in-process import?
10
- * - pi's CLI entry has top-level side effects (TUI bootstrap, signal handlers,
11
- * raw stdin mode). Running it in-process means our wrapper process owns those,
12
- * and any future pre-processing we add (skills, system prompts) becomes
13
- * entangled with pi's lifecycle.
14
- * - A child process gives us clean exit-code propagation, clean SIGINT
15
- * forwarding, and a stable boundary for future iterations to inject things
16
- * like extra args, env vars, or post-run hooks without monkey-patching pi.
17
- * - stdio: "inherit" gives pi direct TTY access, so its TUI renders correctly
18
- * and raw stdin works as expected.
5
+ * Headless CLI wrapper no TUI mode supported.
19
6
  *
20
7
  * Subcommand routing:
21
8
  * - `spectral login` → interactive auth flow (writes ~/.spectral/config.json)
22
9
  * - `spectral logout` → deletes ~/.spectral/config.json
10
+ * - `spectral serve` → start the Aexol relay WebSocket server (headless)
11
+ * - `spectral bind` → link directory to an Aexol Studio project
12
+ * - `spectral unbind` → remove Aexol Studio project binding
23
13
  * - `spectral --version` / `--help` → branded short-circuits, no auth needed.
24
- * - anything else → pre-flight: require ~/.spectral/config.json, then spawn pi
25
- * with the bundled Aexol MCP extension auto-loaded.
14
+ * - anything else → error with usage hint.
26
15
  */
27
- import { spawn } from "node:child_process";
28
- import { existsSync, readFileSync } from "node:fs";
29
- import { constants as osConstants } from "node:os";
16
+ import { readFileSync } from "node:fs";
30
17
  import { dirname, resolve } from "node:path";
31
18
  import { fileURLToPath } from "node:url";
32
- import { requireLogin } from "./preflight.js";
33
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
34
20
  // ---- Read our own version ----------------------------------------------------
35
21
  const pkgPath = resolve(__dirname, "..", "package.json");
36
22
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
37
23
  const VERSION = pkg.version;
38
24
  const TAGLINE = "Coding agent that never sleeps";
39
- // ---- Resolve pi's bin --------------------------------------------------------
40
- // pi exports `.` as ESM only and does not export `./package.json`, so
41
- // require.resolve(...) is unreliable across Node versions. Instead we walk
42
- // upward from this file's location looking for a `node_modules/<pi>/package.json`.
43
- function resolvePiBin() {
44
- const piPkgRel = "node_modules/@mariozechner/pi-coding-agent/package.json";
45
- let dir = __dirname;
46
- let piPkgJsonPath;
47
- for (let i = 0; i < 20; i++) {
48
- const candidate = resolve(dir, piPkgRel);
49
- try {
50
- readFileSync(candidate, "utf8");
51
- piPkgJsonPath = candidate;
52
- break;
53
- }
54
- catch {
55
- /* keep walking */
56
- }
57
- const parent = dirname(dir);
58
- if (parent === dir)
59
- break;
60
- dir = parent;
61
- }
62
- if (!piPkgJsonPath) {
63
- throw new Error("Unable to locate @mariozechner/pi-coding-agent in any ancestor node_modules.");
64
- }
65
- const piPkg = JSON.parse(readFileSync(piPkgJsonPath, "utf8"));
66
- let binRel;
67
- if (typeof piPkg.bin === "string") {
68
- binRel = piPkg.bin;
69
- }
70
- else if (piPkg.bin && typeof piPkg.bin === "object") {
71
- binRel = piPkg.bin.pi ?? Object.values(piPkg.bin)[0];
72
- }
73
- if (!binRel) {
74
- throw new Error("Unable to locate pi bin in @mariozechner/pi-coding-agent package.json");
75
- }
76
- return resolve(dirname(piPkgJsonPath), binRel);
77
- }
78
- /**
79
- * Resolve an extension entry-point path, falling back from .js to .ts.
80
- *
81
- * In production __dirname is dist/ and compiled .js files exist. In dev
82
- * (tsx src/cli.ts) __dirname is src/ and only .ts sources are present.
83
- * pi's extension loader uses jiti which handles .ts transparently.
84
- */
85
- function resolveExtensionEntry(relativePath, fileName) {
86
- const dir = resolve(__dirname, relativePath);
87
- const jsPath = resolve(dir, fileName + ".js");
88
- if (existsSync(jsPath))
89
- return jsPath;
90
- const tsPath = resolve(dir, fileName + ".ts");
91
- if (existsSync(tsPath))
92
- return tsPath;
93
- return jsPath; // let the missing-file check produce a clear error
94
- }
95
- /** Absolute path to the bundled aexol-mcp extension. */
96
- function resolveAexolExtensionPath() {
97
- return resolveExtensionEntry("extensions", "aexol-mcp");
98
- }
99
- /** Absolute path to the bundled observations-memory extension. */
100
- function resolveObservationalMemoryPath() {
101
- return resolveExtensionEntry("memory", "index");
102
- }
103
- /** Absolute path to the bundled pi-mcp-adapter extension. */
104
- function resolveMcpExtensionPath() {
105
- return resolveExtensionEntry("mcp", "index");
106
- }
107
- /** Absolute path to the bundled openrouter-attribution extension. */
108
- function resolveOpenRouterAttributionPath() {
109
- return resolveExtensionEntry("extensions", "openrouter-attribution");
110
- }
111
- /** Absolute path to the bundled agent subagent extension. */
112
- function resolveAgentExtensionPath() {
113
- return resolveExtensionEntry("agent", "index");
114
- }
115
- /** Absolute path to the bundled designer extension. */
116
- function resolveDesignerExtensionPath() {
117
- return resolveExtensionEntry("designer", "index");
118
- }
119
25
  // ---- Branded helpers ---------------------------------------------------------
120
26
  function printVersion() {
121
27
  process.stdout.write(`spectral ${VERSION} — ${TAGLINE}\n`);
@@ -128,77 +34,29 @@ function printHeader() {
128
34
  "Subcommands:",
129
35
  " spectral login Interactive authentication (team API key or OAuth)",
130
36
  " spectral logout Remove stored Aexol credentials",
131
- " spectral serve Connect this machine to the Aexol relay backend",
132
- " spectral bind Link this directory to an Aexol Studio project",
133
- " spectral unbind Remove the Aexol Studio project binding",
37
+ " spectral serve Connect this machine to the Aexol relay backend",
38
+ " spectral bind Link this directory to an Aexol Studio project",
39
+ " spectral unbind Remove the Aexol Studio project binding",
40
+ " spectral --version Print version and exit",
41
+ " spectral --help Print this help and exit",
134
42
  "",
135
- "Powered by pi (@mariozechner/pi-coding-agent).",
136
- "All other flags are forwarded to pi. Run: spectral <pi-args>",
43
+ "TUI/interactive mode has been removed. Use `spectral serve` and connect",
44
+ "via the Aexol Studio web frontend or relay WebSocket.",
137
45
  "",
138
46
  ].join("\n"));
139
47
  }
140
- // ---- Delegate to pi ----------------------------------------------------------
141
- function delegateToPi(args) {
142
- const piBin = resolvePiBin();
143
- // The subprocess inherits a snapshot of process.env (which already has
144
- // all interactive-editor vars disabled by disableInteractiveGitEditors()),
145
- // but we pass it explicitly anyway as defense-in-depth in case the user
146
- // overrides any of these in their shell rc files before launching spectral.
147
- const child = spawn(process.execPath, [piBin, ...args], {
148
- stdio: "inherit",
149
- env: {
150
- ...process.env,
151
- GIT_EDITOR: "true",
152
- GIT_SEQUENCE_EDITOR: "true",
153
- EDITOR: "true",
154
- VISUAL: "true",
155
- GIT_PAGER: "cat",
156
- },
157
- });
158
- // Forward common termination signals to pi so its TUI can clean up.
159
- const signals = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
160
- for (const sig of signals) {
161
- process.on(sig, () => {
162
- if (!child.killed) {
163
- try {
164
- child.kill(sig);
165
- }
166
- catch {
167
- /* ignore */
168
- }
169
- }
170
- });
171
- }
172
- child.on("exit", (code, signal) => {
173
- if (signal) {
174
- const sigNum = osConstants.signals[signal] ?? 0;
175
- process.exit(128 + sigNum);
176
- }
177
- process.exit(code ?? 0);
178
- });
179
- child.on("error", (err) => {
180
- process.stderr.write(`spectral: failed to launch pi: ${err.message}\n`);
181
- process.exit(1);
182
- });
183
- // spawn returns; keep the type system happy.
184
- // We never actually reach here because of the exit handlers above, but
185
- // TypeScript needs a `never` terminator.
186
- return undefined;
187
- }
188
48
  // ---- Disable interactive git editors ----------------------------------------
189
- // Set at process level so BOTH direct mode (delegateToPisubprocess inherits)
190
- // AND serve mode (PiBridge in-process getShellEnv() returns process.env) are
191
- // covered. Without this, `git rebase --continue`, `git commit` (without -m),
192
- // `git merge`, etc. open an editor that hangs forever the agent's bash tool
193
- // runs with `stdio: ["ignore", ...]`, so there is no TTY to interact with.
49
+ // Set at process level for serve mode (PiBridge in-process getShellEnv()
50
+ // returns process.env). Without this, `git rebase --continue`, `git commit`
51
+ // (without -m), `git merge`, etc. open an editor that hangs forever — the
52
+ // agent's bash tool runs with `stdio: ["ignore", ...]`, so there is no TTY
53
+ // to interact with.
194
54
  function disableInteractiveGitEditors() {
195
55
  const editorVars = {
196
56
  GIT_EDITOR: "true",
197
57
  GIT_SEQUENCE_EDITOR: "true",
198
58
  EDITOR: "true",
199
59
  VISUAL: "true",
200
- // cat pipes output straight through; prevents git's pager (less) from
201
- // blocking on a non-existent TTY.
202
60
  GIT_PAGER: "cat",
203
61
  };
204
62
  for (const [key, value] of Object.entries(editorVars)) {
@@ -209,7 +67,6 @@ function disableInteractiveGitEditors() {
209
67
  }
210
68
  // ---- Main --------------------------------------------------------------------
211
69
  async function main() {
212
- // Must be called before any pi process is spawned (direct or serve).
213
70
  disableInteractiveGitEditors();
214
71
  const args = process.argv.slice(2);
215
72
  const first = args[0];
@@ -220,12 +77,7 @@ async function main() {
220
77
  }
221
78
  if (first === "--help" || first === "-h") {
222
79
  printHeader();
223
- // Spawn pi to append its own help, then bail. delegateToPi is
224
- // annotated `: never` because its child.on("exit") handler calls
225
- // process.exit, but the function itself returns synchronously after
226
- // spawn — so we must not fall through to the pre-flight check below.
227
- delegateToPi(args);
228
- return;
80
+ process.exit(0);
229
81
  }
230
82
  // Subcommands. Dynamic import keeps the cold-start path light when users
231
83
  // are just running pi.
@@ -257,59 +109,12 @@ async function main() {
257
109
  await runUnbind();
258
110
  process.exit(0);
259
111
  }
260
- // Pre-flight: every other invocation requires authenticated state. We do NOT
261
- // auto-launch the login flow — that would fight pi's stdio inheritance and
262
- // hide auth state changes from the user.
263
- await requireLogin();
264
- // Resolve and inject the Aexol MCP extension. Sanity-check existence so we
265
- // fail with a clear message if someone publishes a broken bundle.
266
- const aexolExtPath = resolveAexolExtensionPath();
267
- if (!existsSync(aexolExtPath)) {
268
- process.stderr.write(`spectral: bundled Aexol MCP extension not found at ${aexolExtPath}. This is a packaging bug.\n`);
269
- process.exit(1);
270
- }
271
- // Bundled pi-mcp-adapter extension for standard MCP server support
272
- // (stdio + SSE/HTTP transports, lazy loading, OAuth, /mcp panel).
273
- const mcpExtPath = resolveMcpExtensionPath();
274
- if (!existsSync(mcpExtPath)) {
275
- process.stderr.write(`spectral: bundled MCP extension not found at ${mcpExtPath}. This is a packaging bug.\n`);
276
- process.exit(1);
277
- }
278
- // Bundled observational memory extension for tiered agent memory
279
- // (observer → reflector → pruner pipeline with /om-status, /om-view, recall tool).
280
- const obsMemPath = resolveObservationalMemoryPath();
281
- if (!existsSync(obsMemPath)) {
282
- process.stderr.write(`spectral: bundled observational-memory extension not found at ${obsMemPath}. This is a packaging bug.\n`);
283
- process.exit(1);
284
- }
285
- // OpenRouter attribution extension – adds headers so Aexol appears in OpenRouter rankings
286
- const openrouterAttrPath = resolveOpenRouterAttributionPath();
287
- if (!existsSync(openrouterAttrPath)) {
288
- process.stderr.write(`spectral: bundled OpenRouter attribution extension not found at ${openrouterAttrPath}. This is a packaging bug.\n`);
289
- process.exit(1);
290
- }
291
- // Bundled agent delegation extension for subagent task delegation
292
- const agentExtPath = resolveAgentExtensionPath();
293
- if (!existsSync(agentExtPath)) {
294
- process.stderr.write(`spectral: bundled agent extension not found at ${agentExtPath}. This is a packaging bug.\n`);
295
- process.exit(1);
296
- }
297
- // Bundled designer extension for design tasks (prototypes, slide decks, mobile apps, etc.)
298
- const designerExtPath = resolveDesignerExtensionPath();
299
- if (!existsSync(designerExtPath)) {
300
- process.stderr.write(`spectral: bundled designer extension not found at ${designerExtPath}. This is a packaging bug.\n`);
301
- process.exit(1);
302
- }
303
- const extFlags = [
304
- "--extension", aexolExtPath,
305
- "--extension", mcpExtPath,
306
- "--extension", obsMemPath,
307
- "--extension", openrouterAttrPath,
308
- "--extension", agentExtPath,
309
- "--extension", designerExtPath,
310
- ];
311
- const finalArgs = [...extFlags, ...args];
312
- delegateToPi(finalArgs);
112
+ // No TUI/interactive mode is supported. All other invocations must use
113
+ // explicit subcommands.
114
+ process.stderr.write(`spectral: unknown command "${first ?? ""}".\n` +
115
+ `Use "spectral --help" for available subcommands.\n` +
116
+ `To connect this machine, run: spectral serve\n`);
117
+ process.exit(1);
313
118
  }
314
119
  main().catch((err) => {
315
120
  const msg = err instanceof Error ? err.message : String(err);