@aexol/spectral 0.8.0 → 0.8.2

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 (43) hide show
  1. package/dist/extensions/kanban-bridge.js +668 -0
  2. package/dist/extensions/spectral-vision-fallback.js +3 -2
  3. package/dist/mcp/init.js +1 -9
  4. package/dist/memory/index.js +2 -0
  5. package/dist/memory/tools/write-project-observation.js +60 -0
  6. package/dist/relay/auto-research.js +34 -0
  7. package/dist/sdk/ai/env-api-keys.js +9 -49
  8. package/dist/sdk/ai/utils/oauth/anthropic.js +1 -1
  9. package/dist/sdk/ai/utils/oauth/openai-codex.js +1 -1
  10. package/dist/sdk/coding-agent/config.js +2 -69
  11. package/dist/sdk/coding-agent/core/extensions/loader.js +2 -35
  12. package/dist/sdk/coding-agent/core/extensions/runner.js +1 -2
  13. package/dist/sdk/coding-agent/core/model-resolver-utils.js +8 -0
  14. package/dist/sdk/coding-agent/core/model-resolver.js +1 -1
  15. package/dist/sdk/coding-agent/core/resource-loader.js +1 -1
  16. package/dist/sdk/coding-agent/core/settings-manager.js +1 -170
  17. package/dist/sdk/coding-agent/core/system-prompt.js +3 -1
  18. package/dist/sdk/coding-agent/core/theme.js +202 -0
  19. package/dist/sdk/coding-agent/core/tools/bash.js +17 -18
  20. package/dist/sdk/coding-agent/core/tools/edit.js +7 -8
  21. package/dist/sdk/coding-agent/core/tools/find.js +9 -13
  22. package/dist/sdk/coding-agent/core/tools/grep.js +10 -14
  23. package/dist/sdk/coding-agent/core/tools/ls.js +9 -10
  24. package/dist/sdk/coding-agent/core/tools/read.js +15 -25
  25. package/dist/sdk/coding-agent/{modes/interactive/components/diff.js → core/tools/render-diff.js} +18 -31
  26. package/dist/sdk/coding-agent/core/tools/write.js +10 -11
  27. package/dist/sdk/coding-agent/index.js +7 -5
  28. package/dist/sdk/coding-agent/modes/index.js +0 -1
  29. package/dist/sdk/coding-agent/modes/rpc/rpc-mode.js +2 -2
  30. package/dist/sdk/coding-agent/utils/photon.js +2 -10
  31. package/dist/sdk/coding-agent/utils/pi-user-agent.js +1 -2
  32. package/dist/server/agent-bridge.js +2 -1
  33. package/package.json +1 -1
  34. package/dist/sdk/coding-agent/bun/cli.js +0 -7
  35. package/dist/sdk/coding-agent/bun/restore-sandbox-env.js +0 -31
  36. package/dist/sdk/coding-agent/cli/args.js +0 -340
  37. package/dist/sdk/coding-agent/cli/file-processor.js +0 -82
  38. package/dist/sdk/coding-agent/cli/initial-message.js +0 -21
  39. package/dist/sdk/coding-agent/core/footer-data-provider.js +0 -309
  40. package/dist/sdk/coding-agent/modes/interactive/components/keybinding-hints.js +0 -35
  41. package/dist/sdk/coding-agent/modes/interactive/components/visual-truncate.js +0 -26
  42. package/dist/sdk/coding-agent/modes/interactive/interactive-mode.js +0 -3
  43. package/dist/sdk/coding-agent/modes/interactive/theme/theme.js +0 -1022
@@ -3,10 +3,9 @@ import { spawn } from "child_process";
3
3
  import { readFileSync, statSync } from "fs";
4
4
  import path from "path";
5
5
  import { Type } from "typebox";
6
- import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
7
6
  import { ensureTool } from "../../utils/tools-manager.js";
8
7
  import { resolveToCwd } from "./path-utils.js";
9
- import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
8
+ import { getTextOutput, shortenPath, str } from "./render-utils.js";
10
9
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
11
10
  import { DEFAULT_MAX_BYTES, formatSize, GREP_MAX_LINE_LENGTH, truncateHead, truncateLine, } from "./truncate.js";
12
11
  const grepSchema = Type.Object({
@@ -23,24 +22,21 @@ const defaultGrepOperations = {
23
22
  isDirectory: (p) => statSync(p).isDirectory(),
24
23
  readFile: (p) => readFileSync(p, "utf-8"),
25
24
  };
26
- function formatGrepCall(args, theme) {
25
+ function formatGrepCall(args, _theme) {
27
26
  const pattern = str(args?.pattern);
28
27
  const rawPath = str(args?.path);
29
28
  const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
30
29
  const glob = str(args?.glob);
31
30
  const limit = args?.limit;
32
- const invalidArg = invalidArgText(theme);
33
- let text = theme.fg("toolTitle", theme.bold("grep")) +
34
- " " +
35
- (pattern === null ? invalidArg : theme.fg("accent", `/${pattern || ""}/`)) +
36
- theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`);
31
+ const invalidArg = "[invalid]";
32
+ let text = `grep ${pattern === null ? invalidArg : `/${pattern || ""}/`} in ${path === null ? invalidArg : path}`;
37
33
  if (glob)
38
- text += theme.fg("toolOutput", ` (${glob})`);
34
+ text += ` (${glob})`;
39
35
  if (limit !== undefined)
40
- text += theme.fg("toolOutput", ` limit ${limit}`);
36
+ text += ` limit ${limit}`;
41
37
  return text;
42
38
  }
43
- function formatGrepResult(result, options, theme, showImages) {
39
+ function formatGrepResult(result, options, _theme, showImages) {
44
40
  const output = getTextOutput(result, showImages).trim();
45
41
  let text = "";
46
42
  if (output) {
@@ -48,9 +44,9 @@ function formatGrepResult(result, options, theme, showImages) {
48
44
  const maxLines = options.expanded ? lines.length : 15;
49
45
  const displayLines = lines.slice(0, maxLines);
50
46
  const remaining = lines.length - maxLines;
51
- text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
47
+ text += `\n${displayLines.join("\n")}`;
52
48
  if (remaining > 0) {
53
- text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
49
+ text += `\n... (${remaining} more lines)`;
54
50
  }
55
51
  }
56
52
  const matchLimit = result.details?.matchLimitReached;
@@ -64,7 +60,7 @@ function formatGrepResult(result, options, theme, showImages) {
64
60
  warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
65
61
  if (linesTruncated)
66
62
  warnings.push("some lines truncated");
67
- text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
63
+ text += `\n[Truncated: ${warnings.join(", ")}]`;
68
64
  }
69
65
  return text;
70
66
  }
@@ -1,9 +1,8 @@
1
1
  import { existsSync, readdirSync, statSync } from "fs";
2
2
  import nodePath from "path";
3
3
  import { Type } from "typebox";
4
- import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
5
4
  import { resolveToCwd } from "./path-utils.js";
6
- import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
5
+ import { getTextOutput, shortenPath, str } from "./render-utils.js";
7
6
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
8
7
  import { DEFAULT_MAX_BYTES, formatSize, truncateHead } from "./truncate.js";
9
8
  const lsSchema = Type.Object({
@@ -16,18 +15,18 @@ const defaultLsOperations = {
16
15
  stat: statSync,
17
16
  readdir: readdirSync,
18
17
  };
19
- function formatLsCall(args, theme) {
18
+ function formatLsCall(args, _theme) {
20
19
  const rawPath = str(args?.path);
21
20
  const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
22
21
  const limit = args?.limit;
23
- const invalidArg = invalidArgText(theme);
24
- let text = `${theme.fg("toolTitle", theme.bold("ls"))} ${path === null ? invalidArg : theme.fg("accent", path)}`;
22
+ const invalidArg = "[invalid]";
23
+ let text = `ls ${path === null ? invalidArg : path}`;
25
24
  if (limit !== undefined) {
26
- text += theme.fg("toolOutput", ` (limit ${limit})`);
25
+ text += ` (limit ${limit})`;
27
26
  }
28
27
  return text;
29
28
  }
30
- function formatLsResult(result, options, theme, showImages) {
29
+ function formatLsResult(result, options, _theme, showImages) {
31
30
  const output = getTextOutput(result, showImages).trim();
32
31
  let text = "";
33
32
  if (output) {
@@ -35,9 +34,9 @@ function formatLsResult(result, options, theme, showImages) {
35
34
  const maxLines = options.expanded ? lines.length : 20;
36
35
  const displayLines = lines.slice(0, maxLines);
37
36
  const remaining = lines.length - maxLines;
38
- text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
37
+ text += `\n${displayLines.join("\n")}`;
39
38
  if (remaining > 0) {
40
- text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
39
+ text += `\n... (${remaining} more lines)`;
41
40
  }
42
41
  }
43
42
  const entryLimit = result.details?.entryLimitReached;
@@ -48,7 +47,7 @@ function formatLsResult(result, options, theme, showImages) {
48
47
  warnings.push(`${entryLimit} entries limit`);
49
48
  if (truncation?.truncated)
50
49
  warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
51
- text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
50
+ text += `\n[Truncated: ${warnings.join(", ")}]`;
52
51
  }
53
52
  return text;
54
53
  }
@@ -3,13 +3,12 @@ import { constants } from "fs";
3
3
  import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
4
4
  import { Type } from "typebox";
5
5
  import { getReadmePath } from "../../config.js";
6
- import { keyHint, keyText } from "../../modes/interactive/components/keybinding-hints.js";
7
- import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js";
6
+ import { getLanguageFromPath, highlightCode } from "../theme.js";
8
7
  import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
9
8
  import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
10
9
  import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.js";
11
10
  import { resolveReadPath } from "./path-utils.js";
12
- import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.js";
11
+ import { getTextOutput, replaceTabs, shortenPath, str } from "./render-utils.js";
13
12
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
14
13
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "./truncate.js";
15
14
  const readSchema = Type.Object({
@@ -23,19 +22,18 @@ const defaultReadOperations = {
23
22
  access: (path) => fsAccess(path, constants.R_OK),
24
23
  detectImageMimeType: detectSupportedImageMimeTypeFromFile,
25
24
  };
26
- function formatReadLineRange(args, theme) {
25
+ function formatReadLineRange(args, _theme) {
27
26
  if (args?.offset === undefined && args?.limit === undefined)
28
27
  return "";
29
28
  const startLine = args.offset ?? 1;
30
29
  const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
31
- return theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
30
+ return `:${startLine}${endLine ? `-${endLine}` : ""}`;
32
31
  }
33
- function formatReadCall(args, theme) {
32
+ function formatReadCall(args, _theme) {
34
33
  const rawPath = str(args?.file_path ?? args?.path);
35
34
  const path = rawPath !== null ? shortenPath(rawPath) : null;
36
- const invalidArg = invalidArgText(theme);
37
- const pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
38
- return `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}${formatReadLineRange(args, theme)}`;
35
+ const pathDisplay = path === null ? "[invalid]" : path || "...";
36
+ return `read ${pathDisplay}${formatReadLineRange(args)}`;
39
37
  }
40
38
  function trimTrailingEmptyLines(lines) {
41
39
  let end = lines.length;
@@ -85,19 +83,11 @@ function getCompactReadClassification(args, cwd) {
85
83
  }
86
84
  return undefined;
87
85
  }
88
- function formatCompactReadCall(classification, args, theme) {
89
- const expandHint = theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`);
86
+ function formatCompactReadCall(classification, args, _theme) {
90
87
  if (classification.kind === "skill") {
91
- return (theme.fg("customMessageLabel", `\x1b[1m[skill]\x1b[22m `) +
92
- theme.fg("customMessageText", classification.label) +
93
- formatReadLineRange(args, theme) +
94
- expandHint);
88
+ return `[skill] ${classification.label}${formatReadLineRange(args)}`;
95
89
  }
96
- return (theme.fg("toolTitle", theme.bold(`read ${classification.kind}`)) +
97
- " " +
98
- theme.fg("accent", classification.label) +
99
- formatReadLineRange(args, theme) +
100
- expandHint);
90
+ return `read ${classification.kind} ${classification.label}${formatReadLineRange(args)}`;
101
91
  }
102
92
  function formatReadResult(args, result, options, theme, showImages, cwd, isError) {
103
93
  if (!options.expanded && !isError && getCompactReadClassification(args, cwd)) {
@@ -111,20 +101,20 @@ function formatReadResult(args, result, options, theme, showImages, cwd, isError
111
101
  const maxLines = options.expanded ? lines.length : 10;
112
102
  const displayLines = lines.slice(0, maxLines);
113
103
  const remaining = lines.length - maxLines;
114
- let text = `\n${displayLines.map((line) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line)))).join("\n")}`;
104
+ let text = `\n${displayLines.map((line) => replaceTabs(line)).join("\n")}`;
115
105
  if (remaining > 0) {
116
- text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
106
+ text += `\n... (${remaining} more lines)`;
117
107
  }
118
108
  const truncation = result.details?.truncation;
119
109
  if (truncation?.truncated) {
120
110
  if (truncation.firstLineExceedsLimit) {
121
- text += `\n${theme.fg("warning", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`)}`;
111
+ text += `\n[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`;
122
112
  }
123
113
  else if (truncation.truncatedBy === "lines") {
124
- text += `\n${theme.fg("warning", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`)}`;
114
+ text += `\n[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`;
125
115
  }
126
116
  else {
127
- text += `\n${theme.fg("warning", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`)}`;
117
+ text += `\n[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`;
128
118
  }
129
119
  }
130
120
  return text;
@@ -1,5 +1,9 @@
1
+ /**
2
+ * Plain-text diff rendering for LLM consumption.
3
+ * Extracted from the TUI interactive mode diff renderer, with all ANSI
4
+ * styling removed — the output is plain text suitable for LLM input.
5
+ */
1
6
  import * as Diff from "diff";
2
- import { theme } from "../theme/theme.js";
3
7
  /**
4
8
  * Parse diff line to extract prefix, line number, and content.
5
9
  * Format: "+123 content" or "-123 content" or " 123 content" or " ..."
@@ -10,17 +14,9 @@ function parseDiffLine(line) {
10
14
  return null;
11
15
  return { prefix: match[1], lineNum: match[2], content: match[3] };
12
16
  }
13
- /**
14
- * Replace tabs with spaces for consistent rendering.
15
- */
16
17
  function replaceTabs(text) {
17
18
  return text.replace(/\t/g, " ");
18
19
  }
19
- /**
20
- * Compute word-level diff and render with inverse on changed parts.
21
- * Uses diffWords which groups whitespace with adjacent words for cleaner highlighting.
22
- * Strips leading whitespace from inverse to avoid highlighting indentation.
23
- */
24
20
  function renderIntraLineDiff(oldContent, newContent) {
25
21
  const wordDiff = Diff.diffWords(oldContent, newContent);
26
22
  let removedLine = "";
@@ -30,7 +26,6 @@ function renderIntraLineDiff(oldContent, newContent) {
30
26
  for (const part of wordDiff) {
31
27
  if (part.removed) {
32
28
  let value = part.value;
33
- // Strip leading whitespace from the first removed part
34
29
  if (isFirstRemoved) {
35
30
  const leadingWs = value.match(/^(\s*)/)?.[1] || "";
36
31
  value = value.slice(leadingWs.length);
@@ -38,12 +33,11 @@ function renderIntraLineDiff(oldContent, newContent) {
38
33
  isFirstRemoved = false;
39
34
  }
40
35
  if (value) {
41
- removedLine += theme.inverse(value);
36
+ removedLine += `<REMOVED>${value}</REMOVED>`;
42
37
  }
43
38
  }
44
39
  else if (part.added) {
45
40
  let value = part.value;
46
- // Strip leading whitespace from the first added part
47
41
  if (isFirstAdded) {
48
42
  const leadingWs = value.match(/^(\s*)/)?.[1] || "";
49
43
  value = value.slice(leadingWs.length);
@@ -51,7 +45,7 @@ function renderIntraLineDiff(oldContent, newContent) {
51
45
  isFirstAdded = false;
52
46
  }
53
47
  if (value) {
54
- addedLine += theme.inverse(value);
48
+ addedLine += `<ADDED>${value}</ADDED>`;
55
49
  }
56
50
  }
57
51
  else {
@@ -62,10 +56,10 @@ function renderIntraLineDiff(oldContent, newContent) {
62
56
  return { removedLine, addedLine };
63
57
  }
64
58
  /**
65
- * Render a diff string with colored lines and intra-line change highlighting.
66
- * - Context lines: dim/gray
67
- * - Removed lines: red, with inverse on changed tokens
68
- * - Added lines: green, with inverse on changed tokens
59
+ * Render a diff string as plain text with markers.
60
+ * - Context lines: unchanged
61
+ * - Removed lines: prefixed with "-"
62
+ * - Added lines: prefixed with "+"
69
63
  */
70
64
  export function renderDiff(diffText, _options = {}) {
71
65
  const lines = diffText.split("\n");
@@ -75,12 +69,11 @@ export function renderDiff(diffText, _options = {}) {
75
69
  const line = lines[i];
76
70
  const parsed = parseDiffLine(line);
77
71
  if (!parsed) {
78
- result.push(theme.fg("toolDiffContext", line));
72
+ result.push(line);
79
73
  i++;
80
74
  continue;
81
75
  }
82
76
  if (parsed.prefix === "-") {
83
- // Collect consecutive removed lines
84
77
  const removedLines = [];
85
78
  while (i < lines.length) {
86
79
  const p = parseDiffLine(lines[i]);
@@ -89,7 +82,6 @@ export function renderDiff(diffText, _options = {}) {
89
82
  removedLines.push({ lineNum: p.lineNum, content: p.content });
90
83
  i++;
91
84
  }
92
- // Collect consecutive added lines
93
85
  const addedLines = [];
94
86
  while (i < lines.length) {
95
87
  const p = parseDiffLine(lines[i]);
@@ -98,33 +90,28 @@ export function renderDiff(diffText, _options = {}) {
98
90
  addedLines.push({ lineNum: p.lineNum, content: p.content });
99
91
  i++;
100
92
  }
101
- // Only do intra-line diffing when there's exactly one removed and one added line
102
- // (indicating a single line modification). Otherwise, show lines as-is.
103
93
  if (removedLines.length === 1 && addedLines.length === 1) {
104
94
  const removed = removedLines[0];
105
95
  const added = addedLines[0];
106
96
  const { removedLine, addedLine } = renderIntraLineDiff(replaceTabs(removed.content), replaceTabs(added.content));
107
- result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${removedLine}`));
108
- result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${addedLine}`));
97
+ result.push(`-${removed.lineNum} ${removedLine}`);
98
+ result.push(`+${added.lineNum} ${addedLine}`);
109
99
  }
110
100
  else {
111
- // Show all removed lines first, then all added lines
112
101
  for (const removed of removedLines) {
113
- result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`));
102
+ result.push(`-${removed.lineNum} ${replaceTabs(removed.content)}`);
114
103
  }
115
104
  for (const added of addedLines) {
116
- result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`));
105
+ result.push(`+${added.lineNum} ${replaceTabs(added.content)}`);
117
106
  }
118
107
  }
119
108
  }
120
109
  else if (parsed.prefix === "+") {
121
- // Standalone added line
122
- result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`));
110
+ result.push(`+${parsed.lineNum} ${replaceTabs(parsed.content)}`);
123
111
  i++;
124
112
  }
125
113
  else {
126
- // Context line
127
- result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`));
114
+ result.push(` ${parsed.lineNum} ${replaceTabs(parsed.content)}`);
128
115
  i++;
129
116
  }
130
117
  }
@@ -1,11 +1,10 @@
1
1
  import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises";
2
2
  import { dirname } from "path";
3
3
  import { Type } from "typebox";
4
- import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
5
- import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js";
4
+ import { getLanguageFromPath, highlightCode } from "../theme.js";
6
5
  import { withFileMutationQueue } from "./file-mutation-queue.js";
7
6
  import { resolveToCwd } from "./path-utils.js";
8
- import { invalidArgText, normalizeDisplayText, replaceTabs, shortenPath, str } from "./render-utils.js";
7
+ import { normalizeDisplayText, replaceTabs, shortenPath, str } from "./render-utils.js";
9
8
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
10
9
  const writeSchema = Type.Object({
11
10
  path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
@@ -22,14 +21,14 @@ function trimTrailingEmptyLines(lines) {
22
21
  }
23
22
  return lines.slice(0, end);
24
23
  }
25
- function formatWriteCall(args, options, theme) {
24
+ function formatWriteCall(args, options, _theme) {
26
25
  const rawPath = str(args?.file_path ?? args?.path);
27
26
  const fileContent = str(args?.content);
28
27
  const path = rawPath !== null ? shortenPath(rawPath) : null;
29
- const invalidArg = invalidArgText(theme);
30
- let text = `${theme.fg("toolTitle", theme.bold("write"))} ${path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...")}`;
28
+ const pathDisplay = path === null ? "[invalid]" : path || "...";
29
+ let text = `write ${pathDisplay}`;
31
30
  if (fileContent === null) {
32
- text += `\n\n${theme.fg("error", "[invalid content arg - expected string]")}`;
31
+ text += `\n\n[invalid content arg - expected string]`;
33
32
  }
34
33
  else if (fileContent) {
35
34
  const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
@@ -41,14 +40,14 @@ function formatWriteCall(args, options, theme) {
41
40
  const maxLines = options.expanded ? lines.length : 10;
42
41
  const displayLines = lines.slice(0, maxLines);
43
42
  const remaining = lines.length - maxLines;
44
- text += `\n\n${displayLines.map((line) => (lang ? line : theme.fg("toolOutput", replaceTabs(line)))).join("\n")}`;
43
+ text += `\n\n${displayLines.join("\n")}`;
45
44
  if (remaining > 0) {
46
- text += `${theme.fg("muted", `\n... (${remaining} more lines, ${totalLines} total,`)} ${keyHint("app.tools.expand", "to expand")})`;
45
+ text += `\n... (${remaining} more lines, ${totalLines} total)`;
47
46
  }
48
47
  }
49
48
  return text;
50
49
  }
51
- function formatWriteResult(result, theme) {
50
+ function formatWriteResult(result, _theme) {
52
51
  if (!result.isError) {
53
52
  return undefined;
54
53
  }
@@ -59,7 +58,7 @@ function formatWriteResult(result, theme) {
59
58
  if (!output) {
60
59
  return undefined;
61
60
  }
62
- return `\n${theme.fg("error", output)}`;
61
+ return `\n${output}`;
63
62
  }
64
63
  export function createWriteToolDefinition(cwd, options) {
65
64
  const ops = options?.operations ?? defaultWriteOperations;
@@ -8,6 +8,8 @@ export { AuthStorage, FileAuthStorageBackend, InMemoryAuthStorageBackend, } from
8
8
  export { calculateContextTokens, collectEntriesForBranchSummary, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, findTurnStartIndex, generateBranchSummary, generateSummary, getLastAssistantUsage, prepareBranchEntries, serializeConversation, shouldCompact, } from "./core/compaction/index.js";
9
9
  export { createEventBus } from "./core/event-bus.js";
10
10
  export { createExtensionRuntime, defineTool, discoverAndLoadExtensions, ExtensionRunner, isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, wrapRegisteredTool, wrapRegisteredTools, } from "./core/extensions/index.js";
11
+ // Footer data provider — no longer exported (TUI-only, serve/relay uses no-ops)
12
+ // export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.js";
11
13
  export { convertToLlm } from "./core/messages.js";
12
14
  export { ModelRegistry } from "./core/model-registry.js";
13
15
  export { DefaultPackageManager } from "./core/package-manager.js";
@@ -28,11 +30,11 @@ export { createBashToolDefinition, createEditToolDefinition, createFindToolDefin
28
30
  // Main entry point
29
31
  // main.ts removed (interactive-only, not used in SDK mode)
30
32
  // Run modes for programmatic SDK usage
31
- export { InteractiveMode, RpcClient, runPrintMode, runRpcMode, } from "./modes/index.js";
32
- // UI components for extensions
33
- // Interactive components removed (not used in SDK mode, TUI dependencies stripped)
34
- // Theme utilities for custom tools and extensions
35
- export { getLanguageFromPath, getMarkdownTheme, getSelectListTheme, getSettingsListTheme, highlightCode, initTheme, Theme, } from "./modes/interactive/theme/theme.js";
33
+ // InteractiveMode removed (TUI-only, not used in serve/relay mode)
34
+ export { RpcClient, runRpcMode, } from "./modes/index.js";
35
+ export { runPrintMode } from "./modes/print-mode.js";
36
+ // Theme utilities for syntax highlighting (serve/relay compatible)
37
+ export { getLanguageFromPath, highlightCode, loadThemeFromPath } from "./core/theme.js";
36
38
  // Clipboard utilities
37
39
  export { copyToClipboard } from "./utils/clipboard.js";
38
40
  export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js";
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Run modes for the coding agent.
3
3
  */
4
- export { InteractiveMode } from "./interactive/interactive-mode.js";
5
4
  export { runPrintMode } from "./print-mode.js";
6
5
  export { RpcClient } from "./rpc/rpc-client.js";
7
6
  export { runRpcMode } from "./rpc/rpc-mode.js";
@@ -13,7 +13,6 @@
13
13
  import * as crypto from "node:crypto";
14
14
  import { takeOverStdout, writeRawStdout } from "../../core/output-guard.js";
15
15
  import { killTrackedDetachedChildren } from "../../utils/shell.js";
16
- import { theme } from "../interactive/theme/theme.js";
17
16
  import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
18
17
  /**
19
18
  * Run in RPC mode.
@@ -200,7 +199,8 @@ export async function runRpcMode(runtimeHost) {
200
199
  return undefined;
201
200
  },
202
201
  get theme() {
203
- return theme;
202
+ // Theme not available in RPC mode; return a stub.
203
+ return {};
204
204
  },
205
205
  getAllThemes() {
206
206
  return [];
@@ -1,16 +1,8 @@
1
1
  /**
2
2
  * Photon image processing wrapper.
3
3
  *
4
- * This module provides a unified interface to @silvia-odwyer/photon-node that works in:
5
- * 1. Node.js (development, npm run build)
6
- * 2. Bun compiled binaries (standalone distribution)
7
- *
8
- * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')
9
- * which bakes the build machine's absolute path into Bun compiled binaries.
10
- *
11
- * Solution:
12
- * 1. Patch fs.readFileSync to redirect missing photon_rs_bg.wasm reads
13
- * 2. Copy photon_rs_bg.wasm next to the executable in build:binary
4
+ * This module provides a unified interface to @silvia-odwyer/photon-node for
5
+ * use in Node.js (development, npm run build).
14
6
  */
15
7
  import { createRequire } from "module";
16
8
  import * as path from "path";
@@ -1,4 +1,3 @@
1
1
  export function getPiUserAgent(version) {
2
- const runtime = process.versions.bun ? `bun/${process.versions.bun}` : `node/${process.version}`;
3
- return `pi/${version} (${process.platform}; ${runtime}; ${process.arch})`;
2
+ return `pi/${version} (${process.platform}; node/${process.version}; ${process.arch})`;
4
3
  }
@@ -55,6 +55,7 @@ import { existsSync, statSync } from "node:fs";
55
55
  import { dirname, join, resolve } from "node:path";
56
56
  import { fileURLToPath } from "node:url";
57
57
  import aexolMcpExtension from "../extensions/aexol-mcp.js";
58
+ import kanbanBridgeExtension from "../extensions/kanban-bridge.js";
58
59
  import spectralVisionExtension from "../extensions/spectral-vision-fallback.js";
59
60
  import subagentExt from "../agent/index.js";
60
61
  import designerExtension from "../designer/index.js";
@@ -418,7 +419,7 @@ export class AgentBridge {
418
419
  async start() {
419
420
  if (this.disposed)
420
421
  throw new Error("AgentBridge already disposed");
421
- const extensionFactories = [aexolMcpExtension, async (pi) => { spectralVisionExtension(pi); }, async (pi) => { subagentExt(pi); }, async (pi) => { designerExtension(pi); }, async (pi) => { observationalMemory(pi); }];
422
+ const extensionFactories = [aexolMcpExtension, kanbanBridgeExtension, async (pi) => { spectralVisionExtension(pi); }, async (pi) => { subagentExt(pi); }, async (pi) => { designerExtension(pi); }, async (pi) => { observationalMemory(pi); }];
422
423
  // Load pi-mcp-adapter via jiti so tsc never crawls its .ts files in
423
424
  // node_modules. The static `import` was causing tsc to type-check
424
425
  // pi-mcp-adapter's source and fail the build on its type errors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aexol/spectral",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "AI coding agent for Aexol with relay-based browser access.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- import { APP_NAME } from "../config.js";
3
- process.title = APP_NAME;
4
- process.emitWarning = (() => { });
5
- import { restoreSandboxEnv } from "./restore-sandbox-env.js";
6
- restoreSandboxEnv();
7
- await import("../cli.js");
@@ -1,31 +0,0 @@
1
- /**
2
- * Workaround for https://github.com/oven-sh/bun/issues/27802
3
- *
4
- * Bun compiled binaries have an empty `process.env` when running inside
5
- * sandbox environments (e.g. nono on Linux/macOS). On Linux we can recover
6
- * the environment from `/proc/self/environ`.
7
- */
8
- import { readFileSync } from "node:fs";
9
- /**
10
- * Restore environment variables from `/proc/self/environ` when running
11
- * inside a sandbox where Bun's `process.env` is empty.
12
- */
13
- export function restoreSandboxEnv() {
14
- if (!process.versions?.bun)
15
- return;
16
- // If process.env already has entries, nothing to fix.
17
- if (Object.keys(process.env).length > 0)
18
- return;
19
- try {
20
- const data = readFileSync("/proc/self/environ", "utf-8");
21
- for (const entry of data.split("\0")) {
22
- const idx = entry.indexOf("=");
23
- if (idx > 0) {
24
- process.env[entry.slice(0, idx)] = entry.slice(idx + 1);
25
- }
26
- }
27
- }
28
- catch {
29
- // /proc/self/environ may not be readable; ignore.
30
- }
31
- }