pudu-ai 0.2.22 → 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.
@@ -0,0 +1,24 @@
1
+ // src/shared/format.ts
2
+ function formatNumber(value, digits = 1) {
3
+ if (value === void 0 || Number.isNaN(value)) return "N/A";
4
+ return value.toFixed(digits);
5
+ }
6
+ function formatPercent(value, digits = 0) {
7
+ if (value === void 0 || Number.isNaN(value)) return "N/A";
8
+ return `${value.toFixed(digits)} %`;
9
+ }
10
+ function formatTokensPerSec(value, digits = 1) {
11
+ if (value === void 0 || Number.isNaN(value)) return "N/A";
12
+ return `${value.toFixed(digits)} t/s`;
13
+ }
14
+ function na(value) {
15
+ if (value === void 0 || value === null || value === "") return "N/A";
16
+ return String(value);
17
+ }
18
+
19
+ export {
20
+ formatNumber,
21
+ formatPercent,
22
+ formatTokensPerSec,
23
+ na
24
+ };
@@ -0,0 +1,39 @@
1
+ // src/integrations/catalog.ts
2
+ var INTEGRATIONS = {
3
+ opencode: {
4
+ id: "opencode",
5
+ docsUrl: "https://docs.ollama.com/integrations/opencode",
6
+ ollamaLaunch: "opencode",
7
+ minGenerationTps: 12,
8
+ allowedGrades: ["S", "A", "B"],
9
+ useCases: ["code", "chat", "reasoning"]
10
+ },
11
+ openclaw: {
12
+ id: "openclaw",
13
+ docsUrl: "https://docs.ollama.com/integrations/openclaw",
14
+ ollamaLaunch: "openclaw",
15
+ minGenerationTps: 12,
16
+ allowedGrades: ["S", "A", "B"],
17
+ useCases: ["code", "chat", "reasoning"]
18
+ },
19
+ hermes: {
20
+ id: "hermes",
21
+ docsUrl: "https://docs.ollama.com/integrations/hermes",
22
+ ollamaLaunch: "hermes",
23
+ minGenerationTps: 12,
24
+ allowedGrades: ["S", "A"],
25
+ useCases: ["code", "chat", "reasoning"]
26
+ },
27
+ claude: {
28
+ id: "claude",
29
+ docsUrl: "https://docs.ollama.com/integrations/claude-code",
30
+ ollamaLaunch: "claude",
31
+ minGenerationTps: 12,
32
+ allowedGrades: ["S", "A", "B"],
33
+ useCases: ["code", "chat", "reasoning"]
34
+ }
35
+ };
36
+
37
+ export {
38
+ INTEGRATIONS
39
+ };
@@ -49,6 +49,9 @@ var en = {
49
49
  repoHarnessEnter: "Enter = pick local model from effort Esc = back",
50
50
  repoHarnessNoPrompt: "(empty prompt \u2014 effort is still from the graph)",
51
51
  repoHarnessModel: "BEST LOCAL MODEL FOR THIS TASK",
52
+ repoHarnessKeys: "1 OpenCode 2 Hermes 3 OpenClaw Enter=OpenCode Esc=back",
53
+ repoHarnessNoTag: "No Ollama tag for this model. N/A \u2014 will not invent a launch command.",
54
+ repoHarnessWroteConfig: "Wrote OpenCode config {file} (local model + pudu-ai MCP tools).",
52
55
  repoHelp: `Pudu Agent Lab \u2014 repository search, graph, harness
53
56
 
54
57
  Usage:
@@ -296,6 +299,9 @@ var es = {
296
299
  repoHarnessEnter: "Enter = elegir modelo local seg\xFAn esfuerzo Esc = atr\xE1s",
297
300
  repoHarnessNoPrompt: "(prompt vac\xEDo \u2014 el esfuerzo sigue saliendo del grafo)",
298
301
  repoHarnessModel: "MEJOR MODELO LOCAL PARA ESTA TAREA",
302
+ repoHarnessKeys: "1 OpenCode 2 Hermes 3 OpenClaw Enter=OpenCode Esc=atr\xE1s",
303
+ repoHarnessNoTag: "Este modelo no tiene tag Ollama. N/A \u2014 no se inventa el comando de launch.",
304
+ repoHarnessWroteConfig: "Config OpenCode escrita en {file} (modelo local + tools MCP pudu-ai).",
299
305
  repoHelp: `Pudu Agent Lab \u2014 b\xFAsqueda, grafo y harness
300
306
 
301
307
  Uso:
@@ -513,30 +519,8 @@ function t(id, vars) {
513
519
  return text;
514
520
  }
515
521
 
516
- // src/shared/format.ts
517
- function formatNumber(value, digits = 1) {
518
- if (value === void 0 || Number.isNaN(value)) return "N/A";
519
- return value.toFixed(digits);
520
- }
521
- function formatPercent(value, digits = 0) {
522
- if (value === void 0 || Number.isNaN(value)) return "N/A";
523
- return `${value.toFixed(digits)} %`;
524
- }
525
- function formatTokensPerSec(value, digits = 1) {
526
- if (value === void 0 || Number.isNaN(value)) return "N/A";
527
- return `${value.toFixed(digits)} t/s`;
528
- }
529
- function na(value) {
530
- if (value === void 0 || value === null || value === "") return "N/A";
531
- return String(value);
532
- }
533
-
534
522
  export {
535
523
  resolveLocale,
536
524
  setLocale,
537
- t,
538
- formatNumber,
539
- formatPercent,
540
- formatTokensPerSec,
541
- na
525
+ t
542
526
  };
@@ -1,9 +1,14 @@
1
+ import {
2
+ INTEGRATIONS
3
+ } from "./chunk-47LK5I2X.js";
1
4
  import {
2
5
  formatNumber,
3
6
  formatTokensPerSec,
4
- na,
7
+ na
8
+ } from "./chunk-473MNTMS.js";
9
+ import {
5
10
  t
6
- } from "./chunk-6NXOK4OD.js";
11
+ } from "./chunk-4VEDGI4L.js";
7
12
  import {
8
13
  commandExists,
9
14
  runCommand,
@@ -1053,42 +1058,6 @@ function tasksText(plans, answers) {
1053
1058
  return [...header, ...blocks].join("\n\n");
1054
1059
  }
1055
1060
 
1056
- // src/integrations/catalog.ts
1057
- var INTEGRATIONS = {
1058
- opencode: {
1059
- id: "opencode",
1060
- docsUrl: "https://docs.ollama.com/integrations/opencode",
1061
- ollamaLaunch: "opencode",
1062
- minGenerationTps: 12,
1063
- allowedGrades: ["S", "A", "B"],
1064
- useCases: ["code", "chat", "reasoning"]
1065
- },
1066
- openclaw: {
1067
- id: "openclaw",
1068
- docsUrl: "https://docs.ollama.com/integrations/openclaw",
1069
- ollamaLaunch: "openclaw",
1070
- minGenerationTps: 12,
1071
- allowedGrades: ["S", "A", "B"],
1072
- useCases: ["code", "chat", "reasoning"]
1073
- },
1074
- hermes: {
1075
- id: "hermes",
1076
- docsUrl: "https://docs.ollama.com/integrations/hermes",
1077
- ollamaLaunch: "hermes",
1078
- minGenerationTps: 12,
1079
- allowedGrades: ["S", "A"],
1080
- useCases: ["code", "chat", "reasoning"]
1081
- },
1082
- claude: {
1083
- id: "claude",
1084
- docsUrl: "https://docs.ollama.com/integrations/claude-code",
1085
- ollamaLaunch: "claude",
1086
- minGenerationTps: 12,
1087
- allowedGrades: ["S", "A", "B"],
1088
- useCases: ["code", "chat", "reasoning"]
1089
- }
1090
- };
1091
-
1092
1061
  // src/integrations/ollama-tags.ts
1093
1062
  var RULES = [
1094
1063
  { test: /qwen\s*3\.5\s*[-:]?\s*8b/i, tag: "qwen3.5:8b" },
@@ -53,26 +53,9 @@ function parseJsonStdout(stdout) {
53
53
  return JSON.parse(slice);
54
54
  }
55
55
 
56
- // src/agent-lab/graph.ts
57
- async function buildRepoGraph(repo, timeoutMs = 6e4) {
58
- const invoked = await invokePuduAgent({ op: "graph", repo }, timeoutMs);
59
- if (invoked.timedOut) throw new Error("Python graph engine timed out");
60
- const parsed = parseJsonStdout(invoked.stdout);
61
- if (!parsed || parsed.ok === false) {
62
- throw new Error(parsed.error ?? "graph failed");
63
- }
64
- return parsed;
65
- }
66
- async function runRepoHarness(repo, task, timeoutMs = 6e4) {
67
- const invoked = await invokePuduAgent({ op: "harness", repo, task }, timeoutMs);
68
- if (invoked.timedOut) throw new Error("Python harness engine timed out");
69
- return parseJsonStdout(invoked.stdout);
70
- }
71
-
72
56
  export {
57
+ pythonRoot,
73
58
  findPython,
74
59
  invokePuduAgent,
75
- parseJsonStdout,
76
- buildRepoGraph,
77
- runRepoHarness
60
+ parseJsonStdout
78
61
  };
@@ -0,0 +1,25 @@
1
+ import {
2
+ invokePuduAgent,
3
+ parseJsonStdout
4
+ } from "./chunk-7KPNGK6A.js";
5
+
6
+ // src/agent-lab/graph.ts
7
+ async function buildRepoGraph(repo, timeoutMs = 6e4) {
8
+ const invoked = await invokePuduAgent({ op: "graph", repo }, timeoutMs);
9
+ if (invoked.timedOut) throw new Error("Python graph engine timed out");
10
+ const parsed = parseJsonStdout(invoked.stdout);
11
+ if (!parsed || parsed.ok === false) {
12
+ throw new Error(parsed.error ?? "graph failed");
13
+ }
14
+ return parsed;
15
+ }
16
+ async function runRepoHarness(repo, task, timeoutMs = 6e4) {
17
+ const invoked = await invokePuduAgent({ op: "harness", repo, task }, timeoutMs);
18
+ if (invoked.timedOut) throw new Error("Python harness engine timed out");
19
+ return parseJsonStdout(invoked.stdout);
20
+ }
21
+
22
+ export {
23
+ buildRepoGraph,
24
+ runRepoHarness
25
+ };
@@ -0,0 +1,118 @@
1
+ import {
2
+ INTEGRATIONS
3
+ } from "./chunk-47LK5I2X.js";
4
+ import {
5
+ findPython,
6
+ pythonRoot
7
+ } from "./chunk-7KPNGK6A.js";
8
+ import {
9
+ t
10
+ } from "./chunk-4VEDGI4L.js";
11
+ import {
12
+ commandExists,
13
+ runCommand
14
+ } from "./chunk-SS7OXXQQ.js";
15
+
16
+ // src/integrations/opencode-config.ts
17
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
18
+ import path from "node:path";
19
+ function mergeOpenCodeHarness(existing, ollamaTag, pythonBin, engineRoot) {
20
+ const prev = existing ?? {};
21
+ const provider = prev.provider ?? {};
22
+ const ollama = provider.ollama ?? {};
23
+ const models = ollama.models ?? {};
24
+ const mcp = prev.mcp ?? {};
25
+ return {
26
+ ...prev,
27
+ $schema: typeof prev.$schema === "string" ? prev.$schema : "https://opencode.ai/config.json",
28
+ model: `ollama/${ollamaTag}`,
29
+ provider: {
30
+ ...provider,
31
+ ollama: {
32
+ ...ollama,
33
+ npm: "@ai-sdk/openai-compatible",
34
+ name: typeof ollama.name === "string" ? ollama.name : "Ollama (local)",
35
+ options: {
36
+ ...ollama.options ?? {},
37
+ baseURL: "http://127.0.0.1:11434/v1"
38
+ },
39
+ models: {
40
+ ...models,
41
+ [ollamaTag]: {
42
+ ...models[ollamaTag] ?? {},
43
+ name: ollamaTag
44
+ }
45
+ }
46
+ }
47
+ },
48
+ mcp: {
49
+ ...mcp,
50
+ "pudu-ai": {
51
+ type: "local",
52
+ enabled: true,
53
+ timeout: 12e4,
54
+ command: [pythonBin, "-m", "pudu_agent", "mcp"],
55
+ environment: { PYTHONPATH: engineRoot, PYTHONUNBUFFERED: "1" }
56
+ }
57
+ }
58
+ };
59
+ }
60
+ async function writeOpenCodeHarnessConfig(input) {
61
+ const file = path.join(input.repo, "opencode.json");
62
+ let existing;
63
+ try {
64
+ const raw = await readFile(file, "utf8");
65
+ const parsed = JSON.parse(raw);
66
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
67
+ existing = parsed;
68
+ }
69
+ } catch {
70
+ existing = void 0;
71
+ }
72
+ const next = mergeOpenCodeHarness(existing, input.ollamaTag, input.pythonBin, pythonRoot());
73
+ await mkdir(input.repo, { recursive: true });
74
+ await writeFile(file, `${JSON.stringify(next, null, 2)}
75
+ `, "utf8");
76
+ return file;
77
+ }
78
+
79
+ // src/integrations/harness-launch.ts
80
+ async function executeHarnessLaunch(req) {
81
+ const def = INTEGRATIONS[req.tool];
82
+ if (!def) return { ok: false, log: t("launchUnknown") };
83
+ const ollama = await commandExists("ollama");
84
+ if (!ollama) return { ok: false, log: t("launchNeedOllama") };
85
+ const lines = [];
86
+ if (req.tool === "opencode") {
87
+ const python = await findPython();
88
+ if (!python) return { ok: false, log: t("repoPythonMissing") };
89
+ const file = await writeOpenCodeHarnessConfig({
90
+ repo: req.repo,
91
+ ollamaTag: req.ollamaTag,
92
+ pythonBin: python
93
+ });
94
+ lines.push(t("repoHarnessWroteConfig", { file }));
95
+ const bin = await commandExists("opencode");
96
+ if (bin) {
97
+ lines.push(`${bin} (${req.ollamaTag})`);
98
+ const launched2 = await runCommand(bin, [], { cwd: req.repo, timeout: 0, stdio: "inherit" });
99
+ if (launched2.exitCode !== 0) {
100
+ return { ok: false, log: `${lines.join("\n")}
101
+ ${launched2.stderr || t("launchExecFail")}` };
102
+ }
103
+ return { ok: true, log: lines.join("\n") };
104
+ }
105
+ }
106
+ const args = ["launch", def.ollamaLaunch, "--model", req.ollamaTag];
107
+ if (def.id === "openclaw" || def.id === "claude") args.push("--yes");
108
+ lines.push(`ollama ${args.join(" ")}`);
109
+ const launched = await runCommand("ollama", args, { timeout: 0, stdio: "inherit", cwd: req.repo });
110
+ if (launched.exitCode !== 0) {
111
+ return { ok: false, log: `${lines.join("\n")}
112
+ ${launched.stderr || t("launchExecFail")}` };
113
+ }
114
+ return { ok: true, log: lines.join("\n") };
115
+ }
116
+ export {
117
+ executeHarnessLaunch
118
+ };
package/dist/cli/index.js CHANGED
@@ -32,12 +32,14 @@ import {
32
32
  runBenchmark,
33
33
  runtimesText,
34
34
  tasksText
35
- } from "./chunk-PX7BJDD3.js";
35
+ } from "./chunk-6BGYI7XY.js";
36
+ import "./chunk-47LK5I2X.js";
37
+ import "./chunk-473MNTMS.js";
36
38
  import {
37
39
  resolveLocale,
38
40
  setLocale,
39
41
  t
40
- } from "./chunk-6NXOK4OD.js";
42
+ } from "./chunk-4VEDGI4L.js";
41
43
  import {
42
44
  commandExists,
43
45
  killAll,
@@ -728,8 +730,14 @@ ${modelsText(session)}`)}
728
730
  );
729
731
  return;
730
732
  }
731
- const { renderDashboard } = await import("./render-SKZLGUOW.js");
732
- await renderDashboard(session, preset, start);
733
+ const { renderDashboard } = await import("./render-Y4DNH7FW.js");
734
+ const pending = await renderDashboard(session, preset, start);
735
+ if (pending) {
736
+ const { executeHarnessLaunch } = await import("./harness-launch-KEUKZZXH.js");
737
+ const result = await executeHarnessLaunch(pending);
738
+ if (!result.ok) process.stderr.write(`${result.log}
739
+ `);
740
+ }
733
741
  }
734
742
  async function run(args2) {
735
743
  setLocale(resolveLocale(args2.lang));
@@ -741,7 +749,7 @@ async function run(args2) {
741
749
  if (args2.verbose) setLogLevel("verbose");
742
750
  if (!args2.color) process.env.NO_COLOR = "1";
743
751
  if (args2.command === "repo") {
744
- const { runRepoCommand } = await import("./repo-cli-TPYCTV5J.js");
752
+ const { runRepoCommand } = await import("./repo-cli-BK76EVBZ.js");
745
753
  return runRepoCommand(args2);
746
754
  }
747
755
  const cleanup = () => {
@@ -16,16 +16,20 @@ import {
16
16
  resultText,
17
17
  runBenchmark,
18
18
  tasksText
19
- } from "./chunk-PX7BJDD3.js";
19
+ } from "./chunk-6BGYI7XY.js";
20
+ import "./chunk-47LK5I2X.js";
20
21
  import {
21
22
  buildRepoGraph
22
- } from "./chunk-LL5FQT7I.js";
23
+ } from "./chunk-XEEGLMVT.js";
23
24
  import {
24
25
  formatNumber,
25
26
  formatPercent,
26
- formatTokensPerSec,
27
+ formatTokensPerSec
28
+ } from "./chunk-473MNTMS.js";
29
+ import "./chunk-7KPNGK6A.js";
30
+ import {
27
31
  t
28
- } from "./chunk-6NXOK4OD.js";
32
+ } from "./chunk-4VEDGI4L.js";
29
33
  import "./chunk-SS7OXXQQ.js";
30
34
 
31
35
  // src/tui/render.tsx
@@ -750,6 +754,7 @@ function pickHarnessModel(session, effort) {
750
754
  return {
751
755
  modelId: null,
752
756
  modelName: null,
757
+ ollamaTag: null,
753
758
  grade: null,
754
759
  measuredTps: null,
755
760
  reason: "No installed models. N/A",
@@ -790,6 +795,7 @@ function pickHarnessModel(session, effort) {
790
795
  return {
791
796
  modelId: best.row.local.id,
792
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),
793
799
  grade: best.grade,
794
800
  measuredTps: best.tps,
795
801
  reason: reasons[effort.label],
@@ -799,7 +805,12 @@ function pickHarnessModel(session, effort) {
799
805
 
800
806
  // src/tui/views/Graph.tsx
801
807
  import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
802
- function GraphView({ session }) {
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) {
803
814
  const [phase, setPhase] = useState4("build");
804
815
  const [graph, setGraph] = useState4();
805
816
  const [error, setError] = useState4("");
@@ -823,21 +834,38 @@ function GraphView({ session }) {
823
834
  live = false;
824
835
  };
825
836
  }, [repo]);
837
+ const pick = useMemo2(() => graph ? pickHarnessModel(props.session, graph.effort) : void 0, [graph, props.session]);
826
838
  useInput4((input, key) => {
827
- if (phase !== "prompt") return;
828
- if (key.return) {
829
- setPhase("done");
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
+ }
830
851
  return;
831
852
  }
832
- if (key.backspace || key.delete) {
833
- setPrompt((value) => value.slice(0, -1));
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");
834
859
  return;
835
860
  }
836
- if (input && !key.ctrl && !key.meta && input !== " ") {
837
- setPrompt((value) => `${value}${input}`.slice(0, 240));
838
- }
861
+ props.onLaunch({
862
+ tool,
863
+ repo,
864
+ task: prompt,
865
+ modelId: pick.modelId,
866
+ ollamaTag: pick.ollamaTag
867
+ });
839
868
  });
840
- const pick = useMemo2(() => graph ? pickHarnessModel(session, graph.effort) : void 0, [graph, session]);
841
869
  if (phase === "build") {
842
870
  return /* @__PURE__ */ jsxs11(Panel, { title: t("repoGraphTitle"), color: "magenta", children: [
843
871
  /* @__PURE__ */ jsx11(Text11, { children: t("repoGraphBuilding") }),
@@ -912,11 +940,14 @@ function GraphView({ session }) {
912
940
  pick?.modelName ? /* @__PURE__ */ jsxs11(Text11, { children: [
913
941
  pick.modelName,
914
942
  " ",
943
+ pick.ollamaTag ?? "\u2014",
944
+ " ",
915
945
  pick.grade ?? "\u2014",
916
946
  " ",
917
947
  pick.measuredTps != null ? `${pick.measuredTps} t/s` : t("notTested")
918
948
  ] }) : /* @__PURE__ */ jsx11(Text11, { color: "yellow", children: pick?.reason ?? "N/A" }),
919
- /* @__PURE__ */ jsx11(Text11, { dimColor: true, children: pick?.reason })
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") })
920
951
  ] })
921
952
  ] });
922
953
  }
@@ -981,7 +1012,16 @@ function App(props) {
981
1012
  screen === "compare" && /* @__PURE__ */ jsx12(CompareView, { session: props.session }),
982
1013
  screen === "history" && /* @__PURE__ */ jsx12(HistoryView, { session: props.session }),
983
1014
  screen === "tasks" && /* @__PURE__ */ jsx12(TasksView, { session: props.session }),
984
- screen === "graph" && /* @__PURE__ */ jsx12(GraphView, { 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
+ ),
985
1025
  screen === "benchmark" && /* @__PURE__ */ jsx12(
986
1026
  BenchmarkView,
987
1027
  {
@@ -999,11 +1039,26 @@ function App(props) {
999
1039
  // src/tui/render.tsx
1000
1040
  import { jsx as jsx13 } from "react/jsx-runtime";
1001
1041
  async function renderDashboard(session, preset, start) {
1002
- const instance = render(/* @__PURE__ */ jsx13(App, { session, preset, start }), {
1003
- exitOnCtrlC: true,
1004
- patchConsole: true
1005
- });
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
+ );
1006
1060
  await instance.waitUntilExit();
1061
+ return pending;
1007
1062
  }
1008
1063
  export {
1009
1064
  renderDashboard
@@ -1,14 +1,18 @@
1
1
  import {
2
2
  buildRepoGraph,
3
+ runRepoHarness
4
+ } from "./chunk-XEEGLMVT.js";
5
+ import {
6
+ na
7
+ } from "./chunk-473MNTMS.js";
8
+ import {
3
9
  findPython,
4
10
  invokePuduAgent,
5
- parseJsonStdout,
6
- runRepoHarness
7
- } from "./chunk-LL5FQT7I.js";
11
+ parseJsonStdout
12
+ } from "./chunk-7KPNGK6A.js";
8
13
  import {
9
- na,
10
14
  t
11
- } from "./chunk-6NXOK4OD.js";
15
+ } from "./chunk-4VEDGI4L.js";
12
16
  import "./chunk-SS7OXXQQ.js";
13
17
 
14
18
  // src/agent-lab/repo-cli.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pudu-ai",
3
- "version": "0.2.22",
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())
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from pudu_agent.graph import build_graph
9
+ from pudu_agent.search import handle_search
10
+
11
+ PROTOCOL = "2024-11-05"
12
+
13
+
14
+ def _read() -> dict[str, Any] | None:
15
+ headers: dict[str, str] = {}
16
+ while True:
17
+ line = sys.stdin.buffer.readline()
18
+ if not line:
19
+ return None
20
+ if line in (b"\r\n", b"\n"):
21
+ break
22
+ decoded = line.decode("utf-8", errors="replace")
23
+ if ":" not in decoded:
24
+ continue
25
+ key, value = decoded.split(":", 1)
26
+ headers[key.strip().lower()] = value.strip()
27
+ length = int(headers.get("content-length") or "0")
28
+ if length <= 0:
29
+ return None
30
+ body = sys.stdin.buffer.read(length)
31
+ return json.loads(body.decode("utf-8"))
32
+
33
+
34
+ def _write(payload: dict[str, Any]) -> None:
35
+ raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
36
+ sys.stdout.buffer.write(f"Content-Length: {len(raw)}\r\n\r\n".encode("ascii"))
37
+ sys.stdout.buffer.write(raw)
38
+ sys.stdout.buffer.flush()
39
+
40
+
41
+ def _result(req_id: Any, result: Any) -> dict[str, Any]:
42
+ return {"jsonrpc": "2.0", "id": req_id, "result": result}
43
+
44
+
45
+ def _error(req_id: Any, message: str) -> dict[str, Any]:
46
+ return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32000, "message": message}}
47
+
48
+
49
+ def _tools() -> list[dict[str, Any]]:
50
+ return [
51
+ {
52
+ "name": "pudu_repo_search",
53
+ "description": "Deterministic repo search via rg/ast-grep/graph. No LLM. Never invents hits.",
54
+ "inputSchema": {
55
+ "type": "object",
56
+ "properties": {
57
+ "repo": {"type": "string"},
58
+ "query": {"type": "string"},
59
+ "intent": {"type": "string"},
60
+ "limit": {"type": "integer"},
61
+ },
62
+ },
63
+ },
64
+ {
65
+ "name": "pudu_repo_graph",
66
+ "description": "Build a Python AST implementation graph. Edges are EXTRACTED or RESOLVED.",
67
+ "inputSchema": {
68
+ "type": "object",
69
+ "properties": {"repo": {"type": "string"}},
70
+ },
71
+ },
72
+ ]
73
+
74
+
75
+ def _call(name: str, args: dict[str, Any]) -> str:
76
+ repo = args.get("repo") or "."
77
+ if name == "pudu_repo_search":
78
+ result = handle_search(
79
+ {
80
+ "op": "search",
81
+ "repo": repo,
82
+ "query": args.get("query"),
83
+ "intent": args.get("intent"),
84
+ "limit": args.get("limit") or 50,
85
+ }
86
+ )
87
+ return json.dumps(result, ensure_ascii=False)
88
+ if name == "pudu_repo_graph":
89
+ graph = build_graph(Path(str(repo)).expanduser().resolve(), persist=True)
90
+ slim = {
91
+ "ok": graph.get("ok"),
92
+ "repo": graph.get("repo"),
93
+ "backend": graph.get("backend"),
94
+ "metrics": graph.get("metrics"),
95
+ "effort": graph.get("effort"),
96
+ "writtenTo": graph.get("writtenTo"),
97
+ "nodeCount": len(graph.get("nodes") or []),
98
+ "edgeCount": len(graph.get("edges") or []),
99
+ }
100
+ return json.dumps(slim, ensure_ascii=False)
101
+ raise ValueError(f"unknown tool: {name}")
102
+
103
+
104
+ def main() -> int:
105
+ while True:
106
+ req = _read()
107
+ if req is None:
108
+ return 0
109
+ method = req.get("method")
110
+ req_id = req.get("id")
111
+ if method == "initialize":
112
+ _write(
113
+ _result(
114
+ req_id,
115
+ {
116
+ "protocolVersion": PROTOCOL,
117
+ "capabilities": {"tools": {}},
118
+ "serverInfo": {"name": "pudu-ai", "version": "0.2.23"},
119
+ },
120
+ )
121
+ )
122
+ continue
123
+ if method == "notifications/initialized" or method == "initialized":
124
+ continue
125
+ if method == "tools/list":
126
+ _write(_result(req_id, {"tools": _tools()}))
127
+ continue
128
+ if method == "tools/call":
129
+ params = req.get("params") or {}
130
+ name = params.get("name")
131
+ arguments = params.get("arguments") or {}
132
+ try:
133
+ text = _call(str(name), arguments if isinstance(arguments, dict) else {})
134
+ _write(_result(req_id, {"content": [{"type": "text", "text": text}]}))
135
+ except Exception as exc:
136
+ _write(_error(req_id, str(exc)))
137
+ continue
138
+ if req_id is not None:
139
+ _write(_error(req_id, f"unknown method: {method}"))
140
+ return 0