pudu-ai 0.2.21 → 0.2.23

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.
@@ -16,21 +16,28 @@ import {
16
16
  resultText,
17
17
  runBenchmark,
18
18
  tasksText
19
- } from "./chunk-6C5EPKRI.js";
19
+ } from "./chunk-6BGYI7XY.js";
20
+ import "./chunk-47LK5I2X.js";
21
+ import {
22
+ buildRepoGraph
23
+ } from "./chunk-XEEGLMVT.js";
20
24
  import {
21
25
  formatNumber,
22
26
  formatPercent,
23
- formatTokensPerSec,
27
+ formatTokensPerSec
28
+ } from "./chunk-473MNTMS.js";
29
+ import "./chunk-7KPNGK6A.js";
30
+ import {
24
31
  t
25
- } from "./chunk-QGEXKMQ5.js";
32
+ } from "./chunk-4VEDGI4L.js";
26
33
  import "./chunk-SS7OXXQQ.js";
27
34
 
28
35
  // src/tui/render.tsx
29
36
  import { render } from "ink";
30
37
 
31
38
  // src/tui/App.tsx
32
- import { useApp as useApp2, useInput as useInput4 } from "ink";
33
- import { useMemo as useMemo2, useRef as useRef2, useState as useState4 } from "react";
39
+ import { useApp as useApp2, useInput as useInput5 } from "ink";
40
+ import { useMemo as useMemo3, useRef as useRef2, useState as useState5 } from "react";
34
41
 
35
42
  // src/tui/views/Dashboard.tsx
36
43
  import { Box as Box2, Text as Text2 } from "ink";
@@ -84,6 +91,7 @@ var NAV = [
84
91
  { key: "M", label: "Models", color: "cyan" },
85
92
  { key: "R", label: "Rec", color: "magenta" },
86
93
  { key: "T", label: "Tasks", color: "blue" },
94
+ { key: "G", label: "Graph", color: "magenta" },
87
95
  { key: "H", label: "HW", color: "white" },
88
96
  { key: "C", label: "Cmp", color: "green" },
89
97
  { key: "L", label: "Log", color: "cyan" },
@@ -729,6 +737,221 @@ function TasksView({ session }) {
729
737
  ] });
730
738
  }
731
739
 
740
+ // src/tui/views/Graph.tsx
741
+ import { Box as Box10, Text as Text11, useInput as useInput4 } from "ink";
742
+ import { useEffect as useEffect2, useMemo as useMemo2, useState as useState4 } from "react";
743
+
744
+ // src/agent-lab/harness.ts
745
+ var GRADE_RANK = { S: 0, A: 1, B: 2, C: 3, D: 4, F: 5 };
746
+ function isCodeish(useCase) {
747
+ const cases = useCase ?? ["chat", "code"];
748
+ return cases.some((item) => item.includes("code") || item.includes("chat"));
749
+ }
750
+ function pickHarnessModel(session, effort) {
751
+ const rows = session.rows.filter((row) => isCodeish(row.catalog?.useCase));
752
+ const pool = rows.length ? rows : session.rows;
753
+ if (!pool.length) {
754
+ return {
755
+ modelId: null,
756
+ modelName: null,
757
+ ollamaTag: null,
758
+ grade: null,
759
+ measuredTps: null,
760
+ reason: "No installed models. N/A",
761
+ origin: null
762
+ };
763
+ }
764
+ const scored = pool.map((row) => ({
765
+ row,
766
+ grade: row.compatibility?.grade ?? "C",
767
+ tps: row.lastBenchmark?.benchmark.generationTokensPerSecond ?? null,
768
+ params: row.catalog?.paramsBillions ?? null
769
+ }));
770
+ scored.sort((a, b) => {
771
+ if (effort.label === "LOW") {
772
+ if (a.tps != null && b.tps != null && a.tps !== b.tps) return b.tps - a.tps;
773
+ if (a.tps != null && b.tps == null) return -1;
774
+ if (a.tps == null && b.tps != null) return 1;
775
+ return (GRADE_RANK[a.grade] ?? 9) - (GRADE_RANK[b.grade] ?? 9);
776
+ }
777
+ if (effort.label === "HIGH" || effort.label === "VERY_HIGH") {
778
+ if (a.params != null && b.params != null && a.params !== b.params) return b.params - a.params;
779
+ if (a.params != null && b.params == null) return -1;
780
+ if (a.params == null && b.params != null) return 1;
781
+ return (GRADE_RANK[a.grade] ?? 9) - (GRADE_RANK[b.grade] ?? 9);
782
+ }
783
+ const grade = (GRADE_RANK[a.grade] ?? 9) - (GRADE_RANK[b.grade] ?? 9);
784
+ if (grade !== 0) return grade;
785
+ if (a.tps != null && b.tps != null) return b.tps - a.tps;
786
+ return 0;
787
+ });
788
+ const best = scored[0];
789
+ const reasons = {
790
+ LOW: "LOW effort: prefer fastest measured local coding model; else best grade.",
791
+ MEDIUM: "MEDIUM effort: prefer best hardware grade, then measured t/s.",
792
+ HIGH: "HIGH effort: prefer larger catalog params when known; else best grade.",
793
+ VERY_HIGH: "VERY_HIGH effort: prefer larger catalog params when known; else best grade."
794
+ };
795
+ return {
796
+ modelId: best.row.local.id,
797
+ modelName: best.row.local.name,
798
+ ollamaTag: resolveOllamaTag(best.row.local.id, best.row.local.name, best.row.catalog?.id, best.row.catalog?.name) ?? (best.row.local.id.includes(":") ? best.row.local.id : null),
799
+ grade: best.grade,
800
+ measuredTps: best.tps,
801
+ reason: reasons[effort.label],
802
+ origin: "DERIVED"
803
+ };
804
+ }
805
+
806
+ // src/tui/views/Graph.tsx
807
+ import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
808
+ var TOOLS2 = [
809
+ { key: "1", id: "opencode", label: "OpenCode" },
810
+ { key: "2", id: "hermes", label: "Hermes" },
811
+ { key: "3", id: "openclaw", label: "OpenClaw" }
812
+ ];
813
+ function GraphView(props) {
814
+ const [phase, setPhase] = useState4("build");
815
+ const [graph, setGraph] = useState4();
816
+ const [error, setError] = useState4("");
817
+ const [prompt, setPrompt] = useState4("");
818
+ const repo = process.cwd();
819
+ useEffect2(() => {
820
+ let live = true;
821
+ void (async () => {
822
+ try {
823
+ const result = await buildRepoGraph(repo);
824
+ if (!live) return;
825
+ setGraph(result);
826
+ setPhase("prompt");
827
+ } catch (err) {
828
+ if (!live) return;
829
+ setError(err instanceof Error ? err.message : String(err));
830
+ setPhase("error");
831
+ }
832
+ })();
833
+ return () => {
834
+ live = false;
835
+ };
836
+ }, [repo]);
837
+ const pick = useMemo2(() => graph ? pickHarnessModel(props.session, graph.effort) : void 0, [graph, props.session]);
838
+ useInput4((input, key) => {
839
+ if (phase === "prompt") {
840
+ if (key.return) {
841
+ setPhase("pick");
842
+ return;
843
+ }
844
+ if (key.backspace || key.delete) {
845
+ setPrompt((value) => value.slice(0, -1));
846
+ return;
847
+ }
848
+ if (input && !key.ctrl && !key.meta && input !== " ") {
849
+ setPrompt((value) => `${value}${input}`.slice(0, 240));
850
+ }
851
+ return;
852
+ }
853
+ if (phase !== "pick" || !pick?.modelId) return;
854
+ const tool = TOOLS2.find((item) => item.key === input)?.id ?? (key.return ? "opencode" : void 0);
855
+ if (!tool) return;
856
+ if (!pick.ollamaTag) {
857
+ setError(t("repoHarnessNoTag"));
858
+ setPhase("error");
859
+ return;
860
+ }
861
+ props.onLaunch({
862
+ tool,
863
+ repo,
864
+ task: prompt,
865
+ modelId: pick.modelId,
866
+ ollamaTag: pick.ollamaTag
867
+ });
868
+ });
869
+ if (phase === "build") {
870
+ return /* @__PURE__ */ jsxs11(Panel, { title: t("repoGraphTitle"), color: "magenta", children: [
871
+ /* @__PURE__ */ jsx11(Text11, { children: t("repoGraphBuilding") }),
872
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: repo })
873
+ ] });
874
+ }
875
+ if (phase === "error") {
876
+ return /* @__PURE__ */ jsx11(Panel, { title: t("repoGraphTitle"), color: "magenta", children: /* @__PURE__ */ jsx11(Text11, { color: "red", children: error }) });
877
+ }
878
+ if (!graph) return /* @__PURE__ */ jsx11(Text11, { children: " " });
879
+ const degree = /* @__PURE__ */ new Map();
880
+ for (const edge of graph.edges) {
881
+ degree.set(edge.source, (degree.get(edge.source) ?? 0) + 1);
882
+ }
883
+ const top = [...graph.nodes].filter((node) => node.type === "file" || node.type === "function" || node.type === "test").sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0)).slice(0, 8);
884
+ return /* @__PURE__ */ jsxs11(Fragment6, { children: [
885
+ /* @__PURE__ */ jsxs11(Panel, { title: t("repoGraphTitle"), color: "magenta", children: [
886
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: fit(graph.repo, 80) }),
887
+ /* @__PURE__ */ jsxs11(Text11, { children: [
888
+ t("repoGraphFiles"),
889
+ " ",
890
+ graph.metrics.fileCount,
891
+ " ",
892
+ t("repoGraphNodes"),
893
+ " ",
894
+ graph.metrics.nodeCount,
895
+ " ",
896
+ t("repoGraphEdges"),
897
+ " ",
898
+ graph.metrics.edgeCount,
899
+ " (",
900
+ graph.metrics.origin,
901
+ ")"
902
+ ] }),
903
+ top.map((node) => /* @__PURE__ */ jsxs11(Box10, { children: [
904
+ /* @__PURE__ */ jsx11(Text11, { color: "cyan", children: fit(node.file ?? node.name, 36) }),
905
+ /* @__PURE__ */ jsxs11(Text11, { children: [
906
+ " ",
907
+ fit(node.name, 18),
908
+ " "
909
+ ] }),
910
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: degree.get(node.id) ?? 0 })
911
+ ] }, node.id)),
912
+ !top.length ? /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: t("repoGraphEmpty") }) : null
913
+ ] }),
914
+ /* @__PURE__ */ jsxs11(Panel, { title: t("repoEffortTitle"), color: "yellow", children: [
915
+ /* @__PURE__ */ jsxs11(Text11, { children: [
916
+ graph.effort.label,
917
+ " ",
918
+ graph.effort.score0to100,
919
+ "/100 (",
920
+ graph.effort.origin,
921
+ ")"
922
+ ] }),
923
+ /* @__PURE__ */ jsxs11(Text11, { dimColor: true, children: [
924
+ t("repoTokens"),
925
+ " ",
926
+ graph.metrics.tokenEstimate ?? "N/A",
927
+ " (",
928
+ graph.metrics.tokenOrigin ?? "N/A",
929
+ ")"
930
+ ] })
931
+ ] }),
932
+ phase === "prompt" ? /* @__PURE__ */ jsxs11(Panel, { title: t("repoHarnessPrompt"), color: "blue", children: [
933
+ /* @__PURE__ */ jsxs11(Text11, { children: [
934
+ prompt || t("repoHarnessPromptHint"),
935
+ /* @__PURE__ */ jsx11(Text11, { color: "cyan", children: "\u258D" })
936
+ ] }),
937
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: t("repoHarnessEnter") })
938
+ ] }) : /* @__PURE__ */ jsxs11(Panel, { title: t("repoHarnessModel"), color: "green", children: [
939
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: prompt || t("repoHarnessNoPrompt") }),
940
+ pick?.modelName ? /* @__PURE__ */ jsxs11(Text11, { children: [
941
+ pick.modelName,
942
+ " ",
943
+ pick.ollamaTag ?? "\u2014",
944
+ " ",
945
+ pick.grade ?? "\u2014",
946
+ " ",
947
+ pick.measuredTps != null ? `${pick.measuredTps} t/s` : t("notTested")
948
+ ] }) : /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: pick?.reason ?? "N/A" }),
949
+ /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: pick?.reason }),
950
+ pick?.ollamaTag ? /* @__PURE__ */ jsx11(Text11, { color: "cyan", children: t("repoHarnessKeys") }) : /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: t("repoHarnessNoTag") })
951
+ ] })
952
+ ] });
953
+ }
954
+
732
955
  // src/tui/keys.ts
733
956
  function globalNav(letter) {
734
957
  if (letter === "b") return { type: "screen", screen: "benchmark" };
@@ -739,11 +962,16 @@ function globalNav(letter) {
739
962
  if (letter === "l") return { type: "screen", screen: "history" };
740
963
  if (letter === "t") return { type: "screen", screen: "tasks" };
741
964
  if (letter === "s") return { type: "screen", screen: "setup" };
965
+ if (letter === "g") return { type: "screen", screen: "graph" };
742
966
  return void 0;
743
967
  }
744
968
  function handleAppKey(screen, input, key) {
745
969
  const letter = input.toLowerCase();
746
970
  if (screen === "benchmark") return { type: "ignore" };
971
+ if (screen === "graph") {
972
+ if (key.escape) return { type: "home" };
973
+ return { type: "ignore" };
974
+ }
747
975
  if (letter === "q") return { type: "quit" };
748
976
  if (key.escape) return screen === "home" ? { type: "quit" } : { type: "home" };
749
977
  if (screen === "setup") {
@@ -757,34 +985,44 @@ function handleAppKey(screen, input, key) {
757
985
  }
758
986
 
759
987
  // src/tui/App.tsx
760
- import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
988
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
761
989
  function App(props) {
762
990
  const { exit } = useApp2();
763
- const [screen, setScreen] = useState4(props.start ?? "home");
991
+ const [screen, setScreen] = useState5(props.start ?? "home");
764
992
  const screenRef = useRef2(screen);
765
993
  screenRef.current = screen;
766
- const [selected, setSelected] = useState4(0);
767
- const benchable = useMemo2(
994
+ const [selected, setSelected] = useState5(0);
995
+ const benchable = useMemo3(
768
996
  () => props.session.models.filter((m) => Boolean(m.artifactPath)),
769
997
  [props.session.models]
770
998
  );
771
- useInput4((input, key) => {
999
+ useInput5((input, key) => {
772
1000
  const result = handleAppKey(screenRef.current, input, key);
773
1001
  if (result.type === "quit") exit();
774
1002
  if (result.type === "home") setScreen("home");
775
1003
  if (result.type === "screen") setScreen(result.screen);
776
1004
  });
777
- return /* @__PURE__ */ jsxs11(Frame, { children: [
778
- screen !== "benchmark" && /* @__PURE__ */ jsx11(Header, {}),
779
- screen === "home" && /* @__PURE__ */ jsx11(Dashboard, { session: props.session }),
780
- screen === "models" && /* @__PURE__ */ jsx11(ModelsView, { session: props.session }),
781
- screen === "hardware" && /* @__PURE__ */ jsx11(HardwareView, { session: props.session }),
782
- screen === "recommend" && /* @__PURE__ */ jsx11(RecommendView, { session: props.session }),
783
- screen === "setup" && /* @__PURE__ */ jsx11(SetupView, { session: props.session }),
784
- screen === "compare" && /* @__PURE__ */ jsx11(CompareView, { session: props.session }),
785
- screen === "history" && /* @__PURE__ */ jsx11(HistoryView, { session: props.session }),
786
- screen === "tasks" && /* @__PURE__ */ jsx11(TasksView, { session: props.session }),
787
- screen === "benchmark" && /* @__PURE__ */ jsx11(
1005
+ return /* @__PURE__ */ jsxs12(Frame, { children: [
1006
+ screen !== "benchmark" && /* @__PURE__ */ jsx12(Header, {}),
1007
+ screen === "home" && /* @__PURE__ */ jsx12(Dashboard, { session: props.session }),
1008
+ screen === "models" && /* @__PURE__ */ jsx12(ModelsView, { session: props.session }),
1009
+ screen === "hardware" && /* @__PURE__ */ jsx12(HardwareView, { session: props.session }),
1010
+ screen === "recommend" && /* @__PURE__ */ jsx12(RecommendView, { session: props.session }),
1011
+ screen === "setup" && /* @__PURE__ */ jsx12(SetupView, { session: props.session }),
1012
+ screen === "compare" && /* @__PURE__ */ jsx12(CompareView, { session: props.session }),
1013
+ screen === "history" && /* @__PURE__ */ jsx12(HistoryView, { session: props.session }),
1014
+ screen === "tasks" && /* @__PURE__ */ jsx12(TasksView, { session: props.session }),
1015
+ screen === "graph" && /* @__PURE__ */ jsx12(
1016
+ GraphView,
1017
+ {
1018
+ session: props.session,
1019
+ onLaunch: (req) => {
1020
+ props.onLaunch?.(req);
1021
+ exit();
1022
+ }
1023
+ }
1024
+ ),
1025
+ screen === "benchmark" && /* @__PURE__ */ jsx12(
788
1026
  BenchmarkView,
789
1027
  {
790
1028
  session: props.session,
@@ -799,13 +1037,28 @@ function App(props) {
799
1037
  }
800
1038
 
801
1039
  // src/tui/render.tsx
802
- import { jsx as jsx12 } from "react/jsx-runtime";
1040
+ import { jsx as jsx13 } from "react/jsx-runtime";
803
1041
  async function renderDashboard(session, preset, start) {
804
- const instance = render(/* @__PURE__ */ jsx12(App, { session, preset, start }), {
805
- exitOnCtrlC: true,
806
- patchConsole: true
807
- });
1042
+ let pending;
1043
+ const instance = render(
1044
+ /* @__PURE__ */ jsx13(
1045
+ App,
1046
+ {
1047
+ session,
1048
+ preset,
1049
+ start,
1050
+ onLaunch: (req) => {
1051
+ pending = req;
1052
+ }
1053
+ }
1054
+ ),
1055
+ {
1056
+ exitOnCtrlC: true,
1057
+ patchConsole: true
1058
+ }
1059
+ );
808
1060
  await instance.waitUntilExit();
1061
+ return pending;
809
1062
  }
810
1063
  export {
811
1064
  renderDashboard
@@ -1,64 +1,22 @@
1
1
  import {
2
- na,
3
- t
4
- } from "./chunk-QGEXKMQ5.js";
2
+ buildRepoGraph,
3
+ runRepoHarness
4
+ } from "./chunk-XEEGLMVT.js";
5
+ import {
6
+ na
7
+ } from "./chunk-473MNTMS.js";
8
+ import {
9
+ findPython,
10
+ invokePuduAgent,
11
+ parseJsonStdout
12
+ } from "./chunk-7KPNGK6A.js";
5
13
  import {
6
- commandExists,
7
- runCommand
8
- } from "./chunk-SS7OXXQQ.js";
14
+ t
15
+ } from "./chunk-4VEDGI4L.js";
16
+ import "./chunk-SS7OXXQQ.js";
9
17
 
10
18
  // src/agent-lab/repo-cli.ts
11
- import path2 from "node:path";
12
-
13
- // src/agent-lab/python-bridge.ts
14
- import { existsSync } from "node:fs";
15
19
  import path from "node:path";
16
- import { fileURLToPath } from "node:url";
17
- function hasEngine(dir) {
18
- return existsSync(path.join(dir, "pudu_agent", "__main__.py"));
19
- }
20
- function pythonRoot() {
21
- const starts = [path.dirname(fileURLToPath(import.meta.url)), process.cwd()];
22
- for (const start of starts) {
23
- let dir = start;
24
- for (let i = 0; i < 8; i++) {
25
- const candidate = path.join(dir, "python");
26
- if (hasEngine(candidate)) return candidate;
27
- const parent = path.dirname(dir);
28
- if (parent === dir) break;
29
- dir = parent;
30
- }
31
- }
32
- return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "python");
33
- }
34
- async function findPython() {
35
- return await commandExists("python3") ?? await commandExists("python");
36
- }
37
- async function invokePuduAgent(request, timeout = 3e4) {
38
- const python = await findPython();
39
- if (!python) {
40
- throw new Error("python3 not found on PATH");
41
- }
42
- const root = pythonRoot();
43
- const payload = `${JSON.stringify(request)}
44
- `;
45
- const result = await runCommand(python, ["-m", "pudu_agent"], {
46
- cwd: root,
47
- env: { ...process.env, PYTHONPATH: root, PYTHONUNBUFFERED: "1" },
48
- input: payload,
49
- timeout
50
- });
51
- return { ...result, python };
52
- }
53
- function parseJsonStdout(stdout) {
54
- const text = stdout.trim();
55
- if (!text) {
56
- throw new Error("Python engine returned empty stdout");
57
- }
58
- const start = text.indexOf("{");
59
- const slice = start >= 0 ? text.slice(start) : text;
60
- return JSON.parse(slice);
61
- }
62
20
 
63
21
  // src/agent-lab/schema.ts
64
22
  import { z } from "zod";
@@ -83,7 +41,7 @@ var searchMatchSchema = z.object({
83
41
  endLine: z.number().nullable(),
84
42
  endColumn: z.number().nullable(),
85
43
  text: z.string(),
86
- strategy: z.enum(["rg", "ast-grep"]),
44
+ strategy: z.enum(["rg", "ast-grep", "graph"]),
87
45
  language: z.string().nullable(),
88
46
  metavariables: z.record(z.string())
89
47
  });
@@ -205,16 +163,41 @@ async function runRepoCommand(args) {
205
163
  print(t("repoHelp"), args.json);
206
164
  return 0;
207
165
  }
166
+ const python = await findPython();
167
+ if (!python) {
168
+ process.stderr.write(`${t("repoPythonMissing")}
169
+ `);
170
+ return 1;
171
+ }
172
+ const repo = path.resolve(args.repo ?? process.cwd());
173
+ if (sub === "graph") {
174
+ try {
175
+ const graph = await buildRepoGraph(repo);
176
+ print(args.json ? graph : formatGraphText(graph), args.json);
177
+ return graph.ok ? 0 : 1;
178
+ } catch (error) {
179
+ return fail(error, args.json);
180
+ }
181
+ }
182
+ if (sub === "harness") {
183
+ const task = args.task ?? args.positional.slice(1).join(" ").trim() ?? "";
184
+ if (!task) {
185
+ process.stderr.write(`${t("repoNeedTask")}
186
+ `);
187
+ return 1;
188
+ }
189
+ try {
190
+ const harness = await runRepoHarness(repo, task);
191
+ print(args.json ? harness : formatGraphText(harness.graph, task), args.json);
192
+ return harness.ok ? 0 : 1;
193
+ } catch (error) {
194
+ return fail(error, args.json);
195
+ }
196
+ }
208
197
  if (sub !== "search") {
209
198
  process.stderr.write(`${t("repoUnknownSubcommand", { sub })}
210
199
  `);
211
200
  process.stderr.write(`${t("repoHelp")}
212
- `);
213
- return 1;
214
- }
215
- const python = await findPython();
216
- if (!python) {
217
- process.stderr.write(`${t("repoPythonMissing")}
218
201
  `);
219
202
  return 1;
220
203
  }
@@ -226,7 +209,6 @@ async function runRepoCommand(args) {
226
209
  return 1;
227
210
  }
228
211
  const intent = asIntent(args.intent);
229
- const repo = path2.resolve(args.repo ?? process.cwd());
230
212
  try {
231
213
  const result = await searchRepo({
232
214
  repo,
@@ -239,17 +221,54 @@ async function runRepoCommand(args) {
239
221
  print(args.json ? result : formatSearchText(result), args.json);
240
222
  return result.ok ? 0 : 1;
241
223
  } catch (error) {
242
- const message = error instanceof Error ? error.message : String(error);
243
- if (args.json) {
244
- print({ ok: false, error: message }, true);
245
- } else {
246
- process.stderr.write(`${message}
224
+ return fail(error, args.json);
225
+ }
226
+ }
227
+ function fail(error, json) {
228
+ const message = error instanceof Error ? error.message : String(error);
229
+ if (json) print({ ok: false, error: message }, true);
230
+ else process.stderr.write(`${message}
247
231
  `);
248
- }
249
- return 1;
232
+ return 1;
233
+ }
234
+ function formatGraphText(graph, task) {
235
+ const e = graph.effort;
236
+ const m = graph.metrics;
237
+ const lines = [
238
+ t("repoGraphTitle"),
239
+ "",
240
+ t("aboutRepoGraph"),
241
+ "",
242
+ `Repo ${graph.repo}`,
243
+ `Backend ${graph.backend}`,
244
+ `Files ${m.fileCount} (${m.origin})`,
245
+ `Nodes ${m.nodeCount} (${m.origin})`,
246
+ `Edges ${m.edgeCount} (${m.origin})`,
247
+ `Tokens ${m.tokenEstimate ?? "N/A"} (${m.tokenOrigin ?? "N/A"})`,
248
+ `Effort ${e.label} ${e.score0to100}/100 (${e.origin})`
249
+ ];
250
+ if (task) lines.push(`Task ${task}`);
251
+ if (graph.writtenTo) lines.push(`Wrote ${graph.writtenTo}`);
252
+ for (const err of graph.errors) {
253
+ lines.push(`Error ${err.tool}: ${err.message} (${err.origin})`);
254
+ }
255
+ lines.push("");
256
+ const degree = /* @__PURE__ */ new Map();
257
+ for (const edge of graph.edges) {
258
+ degree.set(edge.source, (degree.get(edge.source) ?? 0) + 1);
259
+ degree.set(edge.target, (degree.get(edge.target) ?? 0) + 1);
250
260
  }
261
+ const top = [...graph.nodes].filter((n) => n.type === "file" || n.type === "function" || n.type === "test").sort((a, b) => (degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0)).slice(0, 12);
262
+ for (const node of top) {
263
+ const related = graph.edges.filter((edge) => edge.source === node.id).slice(0, 3).map((edge) => `${edge.type}\u2192${edge.symbol ?? edge.target} [${edge.confidence}]`);
264
+ lines.push(`${node.file ?? node.name} ${node.name} (${degree.get(node.id) ?? 0})`);
265
+ for (const rel of related) lines.push(` ${rel}`);
266
+ }
267
+ if (!top.length) lines.push(t("repoGraphEmpty"));
268
+ return lines.join("\n");
251
269
  }
252
270
  export {
271
+ formatGraphText,
253
272
  formatSearchText,
254
273
  runRepoCommand
255
274
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pudu-ai",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "Pudu-AI — local AI hardware & benchmark lab for the terminal",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,4 +1,10 @@
1
- from pudu_agent.search import main
1
+ import sys
2
2
 
3
3
  if __name__ == "__main__":
4
+ if len(sys.argv) > 1 and sys.argv[1] == "mcp":
5
+ from pudu_agent.mcp_server import main as mcp_main
6
+
7
+ raise SystemExit(mcp_main())
8
+ from pudu_agent.search import main
9
+
4
10
  raise SystemExit(main())