@llmtune/cli 0.1.8 → 0.1.9

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.
@@ -7,24 +7,20 @@ exports.webFetchTool = void 0;
7
7
  const https_1 = __importDefault(require("https"));
8
8
  const http_1 = __importDefault(require("http"));
9
9
  const version_1 = require("../../version");
10
+ const sanitize_1 = require("../sanitize");
10
11
  const MAX_RESPONSE_SIZE = 500_000;
11
12
  const TIMEOUT_MS = 30_000;
13
+ const MAX_REDIRECTS = 5;
12
14
  exports.webFetchTool = {
13
15
  spec() {
14
16
  return {
15
17
  name: "web-fetch",
16
- description: "Fetch content from a URL. Returns the response body as text. Supports HTTP and HTTPS. Use for reading web pages, API responses, or documentation.",
18
+ description: "Fetch content from a URL. Returns the response body as text. Supports HTTP and HTTPS.",
17
19
  inputSchema: {
18
20
  type: "object",
19
21
  properties: {
20
- url: {
21
- type: "string",
22
- description: "The URL to fetch (http:// or https://)",
23
- },
24
- method: {
25
- type: "string",
26
- description: "HTTP method (default: GET)",
27
- },
22
+ url: { type: "string", description: "The URL to fetch (http:// or https://)" },
23
+ method: { type: "string", description: "HTTP method (default: GET)" },
28
24
  headers: {
29
25
  type: "object",
30
26
  description: "Optional request headers as key-value pairs",
@@ -35,109 +31,88 @@ exports.webFetchTool = {
35
31
  isReadOnly: true,
36
32
  };
37
33
  },
38
- run(input, _ctx) {
34
+ async run(input, _ctx) {
39
35
  const url = String(input.url ?? "");
40
- const method = String(input.method ?? "GET").toUpperCase();
41
- const headers = input.headers ?? {};
36
+ const method = String(input.method || "GET").toUpperCase();
37
+ const headers = input.headers || {};
38
+ const redirectCount = parseInt(String(input._redirectCount || "0"), 10);
42
39
  if (!url) {
43
- return Promise.resolve({
44
- name: "web-fetch",
45
- output: { error: "url is required" },
46
- isError: true,
47
- });
40
+ return { name: "web-fetch", output: { error: "url is required" }, isError: true };
48
41
  }
49
- let parsedUrl;
50
42
  try {
51
- parsedUrl = new URL(url);
43
+ (0, sanitize_1.sanitizeUrl)(url);
52
44
  }
53
- catch {
54
- return Promise.resolve({
55
- name: "web-fetch",
56
- output: { error: `Invalid URL: ${url}` },
57
- isError: true,
58
- });
45
+ catch (err) {
46
+ return { name: "web-fetch", output: { error: err.message }, isError: true };
59
47
  }
60
- if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
61
- return Promise.resolve({
62
- name: "web-fetch",
63
- output: { error: `Unsupported protocol: ${parsedUrl.protocol}. Use http:// or https://` },
64
- isError: true,
65
- });
48
+ const result = await doFetch(url, method, headers);
49
+ if (result._redirect) {
50
+ if (redirectCount >= MAX_REDIRECTS) {
51
+ return { name: "web-fetch", output: { error: `Too many redirects (max ${MAX_REDIRECTS})` }, isError: true };
52
+ }
53
+ try {
54
+ (0, sanitize_1.sanitizeUrl)(result._redirect);
55
+ }
56
+ catch (err) {
57
+ return { name: "web-fetch", output: { error: `Redirect to blocked URL: ${err.message}` }, isError: true };
58
+ }
59
+ return exports.webFetchTool.run({ ...input, url: result._redirect, _redirectCount: redirectCount + 1 }, _ctx);
66
60
  }
67
- return new Promise((resolve) => {
68
- const lib = parsedUrl.protocol === "https:" ? https_1.default : http_1.default;
69
- const req = lib.request(url, {
70
- method,
71
- headers: {
72
- "User-Agent": `LLMTune-CLI/${version_1.CLI_VERSION}`,
73
- Accept: "text/html,application/json,text/plain,*/*",
74
- ...headers,
75
- },
76
- timeout: TIMEOUT_MS,
77
- }, (res) => {
78
- // Follow redirects (up to 5)
79
- if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
80
- const redirectUrl = new URL(res.headers.location, url).toString();
81
- return Promise.resolve(exports.webFetchTool.run({ ...input, url: redirectUrl }, _ctx)).then(resolve);
82
- }
83
- const chunks = [];
84
- let size = 0;
85
- res.on("data", (chunk) => {
86
- size += chunk.length;
87
- if (size > MAX_RESPONSE_SIZE) {
88
- req.destroy();
89
- resolve({
90
- name: "web-fetch",
91
- output: {
92
- error: `Response too large (${(size / 1024).toFixed(0)}KB). Maximum is ${MAX_RESPONSE_SIZE / 1024}KB.`,
93
- },
94
- isError: true,
95
- });
96
- return;
61
+ return result;
62
+ },
63
+ };
64
+ function doFetch(url, method, headers) {
65
+ return new Promise((resolve) => {
66
+ const parsedUrl = new URL(url);
67
+ const httpModule = parsedUrl.protocol === "https:" ? https_1.default : http_1.default;
68
+ const options = {
69
+ method,
70
+ hostname: parsedUrl.hostname,
71
+ port: parsedUrl.port,
72
+ path: parsedUrl.pathname + parsedUrl.search,
73
+ headers: {
74
+ "User-Agent": `llmtune-cli/${version_1.CLI_VERSION}`,
75
+ ...headers,
76
+ },
77
+ timeout: TIMEOUT_MS,
78
+ };
79
+ const req = httpModule.request(options, (res) => {
80
+ if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
81
+ const redirectUrl = new URL(res.headers.location, url).toString();
82
+ res.resume();
83
+ resolve({ name: "web-fetch", output: {}, isError: false, _redirect: redirectUrl });
84
+ return;
85
+ }
86
+ let body = "";
87
+ let truncated = false;
88
+ res.on("data", (chunk) => {
89
+ if (!truncated) {
90
+ body += chunk.toString();
91
+ if (body.length > MAX_RESPONSE_SIZE) {
92
+ body = body.slice(0, MAX_RESPONSE_SIZE);
93
+ truncated = true;
97
94
  }
98
- chunks.push(chunk);
99
- });
100
- res.on("end", () => {
101
- const body = Buffer.concat(chunks).toString("utf-8");
102
- const truncated = body.length > 50_000;
103
- const content = truncated ? body.slice(0, 50_000) + "\n... (truncated)" : body;
104
- resolve({
105
- name: "web-fetch",
106
- output: {
107
- url,
108
- status: res.statusCode ?? 0,
109
- contentType: res.headers["content-type"] ?? "unknown",
110
- content,
111
- truncated,
112
- },
113
- isError: false,
114
- });
115
- });
116
- res.on("error", (err) => {
117
- resolve({
118
- name: "web-fetch",
119
- output: { error: `Response error: ${err.message}` },
120
- isError: true,
121
- });
122
- });
95
+ }
123
96
  });
124
- req.on("error", (err) => {
97
+ res.on("end", () => {
125
98
  resolve({
126
99
  name: "web-fetch",
127
- output: { error: `Request failed: ${err.message}` },
128
- isError: true,
100
+ output: { url, status: res.statusCode, content: body, truncated },
101
+ isError: false,
129
102
  });
130
103
  });
131
- req.on("timeout", () => {
132
- req.destroy();
133
- resolve({
134
- name: "web-fetch",
135
- output: { error: `Request timed out after ${TIMEOUT_MS / 1000}s` },
136
- isError: true,
137
- });
104
+ res.on("error", (err) => {
105
+ resolve({ name: "web-fetch", output: { error: `Response error: ${err.message}` }, isError: true });
138
106
  });
139
- req.end();
140
107
  });
141
- },
142
- };
108
+ req.on("error", (err) => {
109
+ resolve({ name: "web-fetch", output: { error: `Request error: ${err.message}` }, isError: true });
110
+ });
111
+ req.on("timeout", () => {
112
+ req.destroy();
113
+ resolve({ name: "web-fetch", output: { error: `Request timed out after ${TIMEOUT_MS / 1000}s` }, isError: true });
114
+ });
115
+ req.end();
116
+ });
117
+ }
143
118
  //# sourceMappingURL=web-fetch.js.map
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.writeTool = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
+ const sanitize_1 = require("../sanitize");
39
40
  exports.writeTool = {
40
41
  spec() {
41
42
  return {
@@ -54,18 +55,23 @@ exports.writeTool = {
54
55
  };
55
56
  },
56
57
  run(input, ctx) {
57
- const filePath = String(input.file_path ?? "");
58
+ let filePath;
59
+ try {
60
+ filePath = (0, sanitize_1.sanitizeFilePath)(String(input.file_path ?? ""), ctx.workspaceRoot);
61
+ }
62
+ catch (err) {
63
+ return { name: "write", output: { error: err.message }, isError: true };
64
+ }
58
65
  const content = String(input.content ?? "");
59
66
  const createDirs = input.create_dirs !== false;
60
- const absPath = path.resolve(ctx.cwd, filePath);
61
- if (!absPath.startsWith(path.resolve(ctx.cwd))) {
62
- return { name: "write", output: { error: "Cannot write outside workspace" }, isError: true };
63
- }
64
67
  try {
65
68
  if (createDirs) {
66
- fs.mkdirSync(path.dirname(absPath), { recursive: true });
69
+ const dir = path.dirname(filePath);
70
+ if (!fs.existsSync(dir)) {
71
+ fs.mkdirSync(dir, { recursive: true });
72
+ }
67
73
  }
68
- fs.writeFileSync(absPath, content, "utf-8");
74
+ fs.writeFileSync(filePath, content, "utf-8");
69
75
  return {
70
76
  name: "write",
71
77
  output: { type: "file_written", filePath, bytesWritten: Buffer.byteLength(content) },
@@ -0,0 +1,2 @@
1
+ export declare function renderBanner(): string;
2
+ //# sourceMappingURL=banner.d.ts.map
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.renderBanner = renderBanner;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const version_1 = require("../version");
9
+ /**
10
+ * LLMTune ASCII art logo — shown on CLI startup and `llmtune` with no args.
11
+ * Uses only standard ASCII characters for maximum terminal compatibility.
12
+ */
13
+ const LOGO = [
14
+ " _ _ _ _ __ _ _",
15
+ " | | | | | (_)/ _| | | |",
16
+ " | |__ | |__ | |_| |_ ___ | | |___ _ __",
17
+ " | '_ \\| '_ \\| | | _/ _ \\ | | / _ \\ '__|",
18
+ " | | | | |_) | | | || __/ | || __/ |",
19
+ " |_| |_|_.__/|_|_|_| \\___| |_|\\___|_|",
20
+ ].join("\n");
21
+ function renderBanner() {
22
+ const lines = [];
23
+ lines.push("");
24
+ lines.push(chalk_1.default.cyan(LOGO));
25
+ lines.push(chalk_1.default.dim(` AI CLI Agent · v${version_1.CLI_VERSION} · llmtune.io`));
26
+ lines.push("");
27
+ return lines.join("\n");
28
+ }
29
+ //# sourceMappingURL=banner.js.map
@@ -1,10 +1,13 @@
1
- import { Ora } from "ora";
1
+ /**
2
+ * Streaming display utilities for throttled terminal output.
3
+ * IMPORTANT: Every token is preserved — none are dropped between updates.
4
+ */
2
5
  export declare function createStreamingDisplay(): {
3
6
  onToken(token: string): void;
7
+ /** Call when stream ends to flush any remaining buffered tokens */
4
8
  flush(): void;
5
9
  getBuffer(): string;
6
10
  };
7
- export declare function showSpinner(text: string): Ora;
8
- export declare function formatToolCall(name: string, input: Record<string, unknown>): string;
11
+ export declare function getToolLabel(name: string, input: Record<string, unknown>): string;
9
12
  export declare function formatToolResult(name: string, output: unknown): string;
10
13
  //# sourceMappingURL=streaming.d.ts.map
@@ -1,8 +1,11 @@
1
1
  "use strict";
2
+ /**
3
+ * Streaming display utilities for throttled terminal output.
4
+ * IMPORTANT: Every token is preserved — none are dropped between updates.
5
+ */
2
6
  Object.defineProperty(exports, "__esModule", { value: true });
3
7
  exports.createStreamingDisplay = createStreamingDisplay;
4
- exports.showSpinner = showSpinner;
5
- exports.formatToolCall = formatToolCall;
8
+ exports.getToolLabel = getToolLabel;
6
9
  exports.formatToolResult = formatToolResult;
7
10
  function createStreamingDisplay() {
8
11
  let buffer = "";
@@ -12,14 +15,16 @@ function createStreamingDisplay() {
12
15
  onToken(token) {
13
16
  buffer += token;
14
17
  const now = Date.now();
15
- if (now - lastUpdateTime > UPDATE_INTERVAL_MS) {
16
- process.stdout.write(token);
18
+ if (now - lastUpdateTime >= UPDATE_INTERVAL_MS) {
19
+ process.stdout.write(buffer);
20
+ buffer = "";
17
21
  lastUpdateTime = now;
18
22
  }
19
23
  },
24
+ /** Call when stream ends to flush any remaining buffered tokens */
20
25
  flush() {
21
26
  if (buffer) {
22
- process.stdout.write("\n");
27
+ process.stdout.write(buffer);
23
28
  buffer = "";
24
29
  }
25
30
  },
@@ -28,20 +33,10 @@ function createStreamingDisplay() {
28
33
  },
29
34
  };
30
35
  }
31
- function showSpinner(text) {
32
- const ora = require("ora");
33
- return ora({ text, color: "cyan" }).start();
34
- }
35
- function formatToolCall(name, input) {
36
- const summary = summarizeToolInput(name, input);
37
- return ` \x1b[33m*\x1b[0m ${name}${summary ? `(${summary})` : ""}`;
38
- }
39
- function summarizeToolInput(name, input) {
40
- switch (name.toLowerCase()) {
41
- case "bash": {
42
- const cmd = String(input.command || "");
43
- return cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
44
- }
36
+ function getToolLabel(name, input) {
37
+ switch (name) {
38
+ case "bash":
39
+ return String(input.command || "");
45
40
  case "read":
46
41
  case "write":
47
42
  case "edit":
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@llmtune/cli",
3
- "version": "0.1.8",
4
- "description": "LLMTune CLI -AI CLI Agent powered by llmtune.io",
3
+ "version": "0.1.9",
4
+ "description": "LLMTune CLI - AI CLI Agent powered by llmtune.io",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "llmtune": "dist/index.js"