noah-agent 0.1.0

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/dist/agent/auth-gate.js +23 -0
  4. package/dist/agent/caveman.js +44 -0
  5. package/dist/agent/login.js +59 -0
  6. package/dist/cli.js +130 -0
  7. package/dist/llm/ollama.js +32 -0
  8. package/dist/llm/providers.js +38 -0
  9. package/dist/llm/registry.js +19 -0
  10. package/dist/llm/resolve.js +44 -0
  11. package/dist/modes/rpc.js +13 -0
  12. package/dist/platform/adapter.js +47 -0
  13. package/dist/platform/detect.js +18 -0
  14. package/dist/platform/linux.js +61 -0
  15. package/dist/platform/macos.js +51 -0
  16. package/dist/platform/types.js +5 -0
  17. package/dist/prompt/system.js +52 -0
  18. package/dist/runtime.js +124 -0
  19. package/dist/safety/audit.js +46 -0
  20. package/dist/safety/confirm.js +17 -0
  21. package/dist/safety/extension.js +65 -0
  22. package/dist/safety/policy.js +100 -0
  23. package/dist/sdk.js +32 -0
  24. package/dist/session.js +113 -0
  25. package/dist/sys/health.js +51 -0
  26. package/dist/sys/probe.js +128 -0
  27. package/dist/sys/report.js +55 -0
  28. package/dist/tools/logs.js +24 -0
  29. package/dist/tools/network.js +47 -0
  30. package/dist/tools/package.js +40 -0
  31. package/dist/tools/service.js +45 -0
  32. package/dist/tools/system.js +33 -0
  33. package/dist/tui/app.js +104 -0
  34. package/dist/tui/branding.js +14 -0
  35. package/dist/tui/components/audit-line.js +37 -0
  36. package/dist/tui/components/header.js +33 -0
  37. package/dist/tui/components/noah-footer.js +33 -0
  38. package/dist/tui/components/request-panel.js +23 -0
  39. package/dist/tui/components/response-view.js +17 -0
  40. package/dist/tui/components/safety-block.js +31 -0
  41. package/dist/tui/components/safety-review.js +36 -0
  42. package/dist/tui/components/thinking-view.js +22 -0
  43. package/dist/tui/components/tool-card.js +45 -0
  44. package/dist/tui/components/util.js +3 -0
  45. package/dist/tui/preview.js +33 -0
  46. package/dist/tui/space/app.js +566 -0
  47. package/dist/tui/space/components.js +261 -0
  48. package/dist/tui/space/dashboard.js +63 -0
  49. package/dist/tui/space/theme.js +39 -0
  50. package/dist/ui/ansi.js +93 -0
  51. package/dist/ui/badge.js +31 -0
  52. package/dist/ui/box.js +61 -0
  53. package/dist/ui/preview.js +37 -0
  54. package/dist/ui/render.js +140 -0
  55. package/package.json +68 -0
  56. package/themes/noah-dark-blue.json +85 -0
@@ -0,0 +1,37 @@
1
+ import { dim, green, red, cyan, truncate, UNICODE } from "../../ui/ansi.js";
2
+ /** A single audit-trail line. Degrades gracefully on narrow widths. */
3
+ export class AuditLineComponent {
4
+ tool;
5
+ command;
6
+ ok;
7
+ constructor(tool, command, ok) {
8
+ this.tool = tool;
9
+ this.command = command;
10
+ this.ok = ok;
11
+ }
12
+ render(width) {
13
+ const markCh = this.ok ? (UNICODE ? "✓" : "+") : (UNICODE ? "✗" : "x");
14
+ const mark = this.ok ? green(markCh) : red(markCh);
15
+ // Build progressively in plain units so the line never exceeds `width`.
16
+ let used = 0;
17
+ const out = [];
18
+ const add = (plain, styled) => {
19
+ if (used + plain.length <= width) {
20
+ out.push(styled);
21
+ used += plain.length;
22
+ }
23
+ };
24
+ add("AUDIT ", dim("AUDIT "));
25
+ add(`${markCh} `, `${mark} `);
26
+ if (width >= 50) {
27
+ const ts = new Date().toLocaleTimeString();
28
+ add(`${ts} `, `${dim(ts)} `);
29
+ }
30
+ add(`${this.tool.padEnd(6)} `, `${cyan(this.tool.padEnd(6))} `);
31
+ const room = width - used;
32
+ if (room > 1)
33
+ out.push(dim(truncate(this.command, room)));
34
+ return [out.join("")];
35
+ }
36
+ invalidate() { }
37
+ }
@@ -0,0 +1,33 @@
1
+ import { bold, white, dim, gray, truncate, fg256, UNICODE } from "../../ui/ansi.js";
2
+ import { clamp } from "./util.js";
3
+ /** NOAH wordmark in ANSI Shadow block letters. */
4
+ const LOGO = [
5
+ "███╗ ██╗ ██████╗ █████╗ ██╗ ██╗",
6
+ "████╗ ██║██╔═══██╗██╔══██╗██║ ██║",
7
+ "██╔██╗ ██║██║ ██║███████║███████║",
8
+ "██║╚██╗██║██║ ██║██╔══██║██╔══██║",
9
+ "██║ ╚████║╚██████╔╝██║ ██║██║ ██║",
10
+ "╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝",
11
+ ];
12
+ /** Dark→light blue gradient (256-color) applied per logo row for depth. */
13
+ const GRADIENT = [19, 20, 26, 27, 33, 39];
14
+ const LOGO_WIDTH = Math.max(...LOGO.map((l) => [...l].length));
15
+ const TAGLINE = "Native Operating-system Agentic Harness";
16
+ /**
17
+ * NOAH header — ASCII-art logo (dark-blue gradient) plus tagline.
18
+ * Falls back to a compact wordmark on narrow terminals or when Unicode is off.
19
+ */
20
+ export class HeaderComponent {
21
+ render(width) {
22
+ if (!UNICODE || width < LOGO_WIDTH) {
23
+ const title = `${bold(fg256(39)("NOAH"))} ${dim(TAGLINE)}`;
24
+ const rule = gray((UNICODE ? "─" : "-").repeat(clamp(width, 10, 68)));
25
+ return [truncate(title, width), rule];
26
+ }
27
+ const lines = LOGO.map((row, i) => fg256(GRADIENT[i])(row));
28
+ const tagline = `${bold(fg256(45)("›"))} ${dim(white(TAGLINE))}`;
29
+ const rule = fg256(24)((UNICODE ? "─" : "-").repeat(clamp(width, 10, 72)));
30
+ return ["", ...lines, "", truncate(tagline, width), rule];
31
+ }
32
+ invalidate() { }
33
+ }
@@ -0,0 +1,33 @@
1
+ import { bold, dim, gray, green, yellow, cyan, truncate, UNICODE } from "../../ui/ansi.js";
2
+ const SEP = UNICODE ? " · " : " | ";
3
+ const BRANCH = UNICODE ? "⎇ " : "git:";
4
+ export class NoahFooterComponent {
5
+ footerData;
6
+ getModel;
7
+ opts;
8
+ unsubscribe;
9
+ constructor(footerData, getModel, opts, onChange) {
10
+ this.footerData = footerData;
11
+ this.getModel = getModel;
12
+ this.opts = opts;
13
+ if (onChange)
14
+ this.unsubscribe = footerData.onBranchChange(onChange);
15
+ }
16
+ render(width) {
17
+ const brand = bold(green("NOAH"));
18
+ const safety = this.opts.dryRun ? yellow("dry-run") : green("safety on");
19
+ const model = this.getModel();
20
+ const branch = this.footerData.getGitBranch();
21
+ const parts = [brand, safety];
22
+ if (model)
23
+ parts.push(cyan(model));
24
+ if (branch && branch !== "detached")
25
+ parts.push(dim(`${BRANCH}${branch}`));
26
+ const line = parts.join(gray(SEP));
27
+ return [truncate(line, width)];
28
+ }
29
+ invalidate() { }
30
+ dispose() {
31
+ this.unsubscribe?.();
32
+ }
33
+ }
@@ -0,0 +1,23 @@
1
+ import { drawBox } from "../../ui/box.js";
2
+ import { bold, cyan, truncate } from "../../ui/ansi.js";
3
+ /**
4
+ * REQUEST panel as a pi-tui Component. Responsive: the inner width is derived
5
+ * from the viewport so every rendered line fits within `width`.
6
+ */
7
+ export class RequestPanelComponent {
8
+ text;
9
+ constructor(text) {
10
+ this.text = text;
11
+ }
12
+ render(width) {
13
+ const inner = Math.max(8, Math.min(72, width - 4));
14
+ return drawBox([truncate(this.text, inner)], {
15
+ title: bold(cyan("REQUEST")),
16
+ style: "round",
17
+ accent: cyan,
18
+ width: inner,
19
+ indent: 0,
20
+ }).split("\n");
21
+ }
22
+ invalidate() { }
23
+ }
@@ -0,0 +1,17 @@
1
+ import { bold, white, dim, cyan, green, wordWrap, UNICODE } from "../../ui/ansi.js";
2
+ const bar = UNICODE ? "│" : "|";
3
+ const dot = UNICODE ? "●" : "*";
4
+ /** Streams NOAH's response under a "● NOAH" header. */
5
+ export class ResponseViewComponent {
6
+ text = "";
7
+ append(delta) {
8
+ this.text += delta;
9
+ }
10
+ render(width) {
11
+ const header = `${cyan(bold(dot))} ${bold(white("NOAH"))}`;
12
+ const wrapWidth = Math.max(8, width - 2);
13
+ const lines = wordWrap(this.text, wrapWidth).map((l) => `${dim(bar)} ${green(l)}`);
14
+ return [header, ...lines];
15
+ }
16
+ invalidate() { }
17
+ }
@@ -0,0 +1,31 @@
1
+ import { drawBox } from "../../ui/box.js";
2
+ import { bold, white, red, truncate, UNICODE } from "../../ui/ansi.js";
3
+ import { clamp } from "./util.js";
4
+ /** SAFETY BLOCK — the catastrophic-deny centerpiece (block panel). */
5
+ export class SafetyBlockComponent {
6
+ command;
7
+ reason;
8
+ constructor(command, reason) {
9
+ this.command = command;
10
+ this.reason = reason;
11
+ }
12
+ render(width) {
13
+ const inner = clamp(width - 8, 8, 64);
14
+ const shield = UNICODE ? "⛔ " : "[X] ";
15
+ const cleanReason = this.reason.replace(/^blocked:\s*/i, "");
16
+ const body = [
17
+ bold(white(truncate(this.command, inner))),
18
+ "",
19
+ bold(white(truncate(`BLOCKED ${UNICODE ? "—" : "-"} ${cleanReason}`, inner))),
20
+ white(truncate("Catastrophic. Cannot be overridden.", inner)),
21
+ ];
22
+ return drawBox(body, {
23
+ title: bold(white(`${shield}SAFETY BLOCK`)),
24
+ style: "block",
25
+ accent: red,
26
+ width: inner,
27
+ indent: 0,
28
+ }).split("\n");
29
+ }
30
+ invalidate() { }
31
+ }
@@ -0,0 +1,36 @@
1
+ import { drawBox } from "../../ui/box.js";
2
+ import { bold, white, dim, yellow, truncate, wordWrap, UNICODE } from "../../ui/ansi.js";
3
+ import { badgeLabel } from "../../ui/badge.js";
4
+ import { clamp } from "./util.js";
5
+ /** SAFETY REVIEW — the confirmation centerpiece (heavy box). */
6
+ export class SafetyReviewComponent {
7
+ command;
8
+ reason;
9
+ toolName;
10
+ constructor(command, reason, toolName) {
11
+ this.command = command;
12
+ this.reason = reason;
13
+ this.toolName = toolName;
14
+ }
15
+ render(width) {
16
+ const inner = clamp(width - 4, 8, 72);
17
+ const intro = wordWrap("NOAH wants to run a state-changing command:", inner).map((l) => white(l));
18
+ const reason = truncate(`reason: ${this.reason} ${UNICODE ? "·" : "-"} tool: ${this.toolName}`, inner);
19
+ const body = [
20
+ ...intro,
21
+ "",
22
+ bold(yellow(truncate(this.command || this.toolName, inner))),
23
+ "",
24
+ dim(reason),
25
+ ];
26
+ return drawBox(body, {
27
+ title: bold(yellow("SAFETY REVIEW")),
28
+ status: badgeLabel("warning"),
29
+ style: "heavy",
30
+ accent: yellow,
31
+ width: inner,
32
+ indent: 0,
33
+ }).split("\n");
34
+ }
35
+ invalidate() { }
36
+ }
@@ -0,0 +1,22 @@
1
+ import { bold, dim, italic, gray, fg256, wordWrap, UNICODE } from "../../ui/ansi.js";
2
+ const purple = fg256(141);
3
+ const bar = UNICODE ? "│" : "|";
4
+ const dot = UNICODE ? "◐" : "*";
5
+ /** Streams the model's reasoning under a dim "● THINKING" header. */
6
+ export class ThinkingViewComponent {
7
+ text = "";
8
+ done = false;
9
+ append(delta) {
10
+ this.text += delta;
11
+ }
12
+ finish() {
13
+ this.done = true;
14
+ }
15
+ render(width) {
16
+ const header = `${purple(bold(dot))} ${dim("THINKING")}`;
17
+ const wrapWidth = Math.max(8, width - 2);
18
+ const lines = wordWrap(this.text, wrapWidth).map((l) => `${gray(bar)} ${dim(italic(l))}`);
19
+ return [header, ...lines];
20
+ }
21
+ invalidate() { }
22
+ }
@@ -0,0 +1,45 @@
1
+ import { drawBox } from "../../ui/box.js";
2
+ import { bold, white, dim, green, red, fg256, truncate } from "../../ui/ansi.js";
3
+ import { badgeLabel } from "../../ui/badge.js";
4
+ import { clamp } from "./util.js";
5
+ const orange = fg256(208);
6
+ /** Tool execution card. Status and output are mutable for live updates. */
7
+ export class ToolCardComponent {
8
+ name;
9
+ command;
10
+ status;
11
+ output;
12
+ constructor(name, command, status = "running", output = []) {
13
+ this.name = name;
14
+ this.command = command;
15
+ this.status = status;
16
+ this.output = output;
17
+ }
18
+ setStatus(status) {
19
+ this.status = status;
20
+ }
21
+ setOutput(output) {
22
+ this.output = output;
23
+ }
24
+ render(width) {
25
+ const inner = clamp(width - 4, 8, 72);
26
+ const prefix = this.name === "bash" ? dim("$ ") : "";
27
+ const body = [];
28
+ if (this.command)
29
+ body.push(prefix + truncate(this.command, inner - 2));
30
+ for (const line of this.output.slice(0, 6))
31
+ body.push(dim(truncate(line, inner)));
32
+ if (body.length === 0)
33
+ body.push(dim("(no output)"));
34
+ const accent = this.status === "success" ? green : this.status === "blocked" ? red : orange;
35
+ return drawBox(body, {
36
+ title: bold(white(`TOOL ${dim("·")} ${this.name}`)),
37
+ status: badgeLabel(this.status),
38
+ style: "round",
39
+ accent,
40
+ width: inner,
41
+ indent: 0,
42
+ }).split("\n");
43
+ }
44
+ invalidate() { }
45
+ }
@@ -0,0 +1,3 @@
1
+ export function clamp(v, min, max) {
2
+ return Math.max(min, Math.min(max, v));
3
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Visual eyeball of NOAH TUI components composed inside a pi-tui Container.
3
+ * npm run build && node dist/tui/preview.js
4
+ * Renders statically (no render loop) so we can review the layout.
5
+ */
6
+ import { Container } from "@earendil-works/pi-tui";
7
+ import { HeaderComponent } from "./components/header.js";
8
+ import { RequestPanelComponent } from "./components/request-panel.js";
9
+ import { ThinkingViewComponent } from "./components/thinking-view.js";
10
+ import { ToolCardComponent } from "./components/tool-card.js";
11
+ import { SafetyReviewComponent } from "./components/safety-review.js";
12
+ import { SafetyBlockComponent } from "./components/safety-block.js";
13
+ import { AuditLineComponent } from "./components/audit-line.js";
14
+ import { ResponseViewComponent } from "./components/response-view.js";
15
+ const WIDTH = Number(process.env.WIDTH ?? 72);
16
+ const think = new ThinkingViewComponent();
17
+ think.append("The user wants htop installed. I'll inspect disk first, then install via brew.");
18
+ const resp = new ResponseViewComponent();
19
+ resp.append("Installed **htop**. Your largest file is `demo.mov` (1.2G).");
20
+ const root = new Container();
21
+ for (const c of [
22
+ new HeaderComponent(),
23
+ new RequestPanelComponent("install htop and show my biggest files"),
24
+ think,
25
+ new ToolCardComponent("bash", "du -ah ~ | sort -rh | head -5", "success", ["1.2G ~/Movies/demo.mov"]),
26
+ new SafetyReviewComponent("sudo apt-get install -y htop", "package install", "bash"),
27
+ new AuditLineComponent("bash", "du -ah ~ | sort -rh | head -5", true),
28
+ new SafetyBlockComponent("rm -rf / --no-preserve-root", "blocked: recursive delete of root/home"),
29
+ resp,
30
+ ]) {
31
+ root.addChild(c);
32
+ }
33
+ process.stdout.write(root.render(WIDTH).join("\n") + "\n");