promptpilot 0.1.4 → 0.1.6

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.
package/README.md CHANGED
@@ -75,6 +75,43 @@ ollama pull qwen2.5:3b
75
75
  ollama pull phi3:mini
76
76
  ```
77
77
 
78
+ ## Custom local compressor model
79
+
80
+ PromptPilot ships a `Modelfile` that defines `promptpilot-compressor`, a text-only compression model built on top of `qwen2.5:3b`. It is tuned to output only the compressed prompt with no reasoning, analysis, or commentary.
81
+
82
+ Build and verify it:
83
+
84
+ ```bash
85
+ ollama pull qwen2.5:3b
86
+ ollama create promptpilot-compressor -f ./Modelfile
87
+ ollama run promptpilot-compressor "explain recursion simply"
88
+ ```
89
+
90
+ Use it via the CLI after installing from npm:
91
+
92
+ ```bash
93
+ # Plain output — pipe directly into Claude
94
+ promptpilot optimize "help me refactor this auth middleware" \
95
+ --model promptpilot-compressor \
96
+ --preset code \
97
+ --plain
98
+
99
+ # JSON output with debug info
100
+ promptpilot optimize "help me refactor this auth middleware" \
101
+ --model promptpilot-compressor \
102
+ --preset code \
103
+ --json --debug
104
+
105
+ # With session memory, piped into Claude
106
+ promptpilot optimize "continue the refactor" \
107
+ --model promptpilot-compressor \
108
+ --session repo-refactor \
109
+ --save-context \
110
+ --plain | claude
111
+ ```
112
+
113
+ `promptpilot-compressor` outputs plain text rather than JSON. PromptPilot detects this automatically and falls back to text-only mode, stripping any reasoning leakage before using the output. Explicit `--model` always takes priority over automatic local model selection.
114
+
78
115
  ## Core behavior
79
116
 
80
117
  PromptPilot has two distinct routing layers.
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // src/cli.ts
4
4
  import { readFileSync, realpathSync } from "fs";
5
5
  import { fileURLToPath } from "url";
6
+ import { execSync } from "child_process";
6
7
 
7
8
  // src/errors.ts
8
9
  var InvalidPromptError = class extends Error {
@@ -353,6 +354,17 @@ var OllamaClient = class {
353
354
  }
354
355
  throw new OllamaUnavailableError("Ollama returned JSON that could not be parsed.");
355
356
  }
357
+ async generateJsonWithTextFallback(options, textFallbackHandler) {
358
+ try {
359
+ return await this.generateJson(options);
360
+ } catch {
361
+ const raw = await this.generate({
362
+ ...options,
363
+ format: void 0
364
+ });
365
+ return textFallbackHandler(raw);
366
+ }
367
+ }
356
368
  };
357
369
 
358
370
  // src/core/systemPrompt.ts
@@ -1046,6 +1058,11 @@ var PromptOptimizer = class {
1046
1058
  contextSummary: relevantContext.summary
1047
1059
  });
1048
1060
  }
1061
+ const originalPromptTokens = this.estimator.estimateText(originalPrompt);
1062
+ const promptCompressionSavings = Math.max(0, originalPromptTokens - estimatedTokensAfter.prompt);
1063
+ const contextCompressionSavings = Math.max(0, estimatedTokensBefore.context - estimatedTokensAfter.context);
1064
+ const wrapperOverhead = Math.max(0, estimatedTokensAfter.total - (estimatedTokensAfter.prompt + estimatedTokensAfter.context));
1065
+ const tokenSavings = promptCompressionSavings + contextCompressionSavings;
1049
1066
  return {
1050
1067
  originalPrompt,
1051
1068
  optimizedPrompt,
@@ -1054,7 +1071,10 @@ var PromptOptimizer = class {
1054
1071
  contextSummary: relevantContext.summary,
1055
1072
  estimatedTokensBefore,
1056
1073
  estimatedTokensAfter,
1057
- tokenSavings: Math.max(0, estimatedTokensBefore.total - estimatedTokensAfter.total),
1074
+ tokenSavings,
1075
+ promptCompressionSavings,
1076
+ contextCompressionSavings,
1077
+ wrapperOverhead,
1058
1078
  mode,
1059
1079
  provider,
1060
1080
  model,
@@ -1121,29 +1141,52 @@ Mode: Ultra compression. Minimize tokens aggressively.` : getOptimizationSystemP
1121
1141
  let optimizedPrompt = "";
1122
1142
  let responseChanges = [];
1123
1143
  let responseWarnings = [];
1124
- try {
1125
- const response = await this.client.generateJson({
1126
- systemPrompt,
1127
- prompt: optimizationPrompt,
1128
- timeoutMs,
1129
- model: options.model,
1130
- temperature: this.config.temperature,
1131
- format: "json"
1132
- });
1133
- optimizedPrompt = normalizeWhitespace(response.optimizedPrompt ?? "");
1134
- responseChanges = response.changes ?? [];
1135
- responseWarnings = response.warnings ?? [];
1136
- } catch {
1137
- const raw = await this.client.generate({
1138
- systemPrompt,
1139
- prompt: optimizationPrompt,
1140
- timeoutMs,
1141
- model: options.model,
1142
- temperature: this.config.temperature
1143
- });
1144
- optimizedPrompt = sanitizeTextOptimizationOutput(raw);
1145
- responseChanges = [`Applied text-only Ollama optimization with ${options.model}.`];
1146
- }
1144
+ const generateWithFallback = this.client.generateJsonWithTextFallback ? async () => {
1145
+ const response2 = await this.client.generateJsonWithTextFallback(
1146
+ {
1147
+ systemPrompt,
1148
+ prompt: optimizationPrompt,
1149
+ timeoutMs,
1150
+ model: options.model,
1151
+ temperature: this.config.temperature,
1152
+ format: "json"
1153
+ },
1154
+ (text) => ({
1155
+ optimizedPrompt: sanitizeTextOptimizationOutput(text),
1156
+ changes: [`Applied text-only Ollama optimization with ${options.model}.`],
1157
+ warnings: []
1158
+ })
1159
+ );
1160
+ return response2;
1161
+ } : async () => {
1162
+ try {
1163
+ return await this.client.generateJson({
1164
+ systemPrompt,
1165
+ prompt: optimizationPrompt,
1166
+ timeoutMs,
1167
+ model: options.model,
1168
+ temperature: this.config.temperature,
1169
+ format: "json"
1170
+ });
1171
+ } catch {
1172
+ const raw = await this.client.generate({
1173
+ systemPrompt,
1174
+ prompt: optimizationPrompt,
1175
+ timeoutMs,
1176
+ model: options.model,
1177
+ temperature: this.config.temperature
1178
+ });
1179
+ return {
1180
+ optimizedPrompt: sanitizeTextOptimizationOutput(raw),
1181
+ changes: [`Applied text-only Ollama optimization with ${options.model}.`],
1182
+ warnings: []
1183
+ };
1184
+ }
1185
+ };
1186
+ const response = await generateWithFallback();
1187
+ optimizedPrompt = normalizeWhitespace(response.optimizedPrompt ?? "");
1188
+ responseChanges = response.changes ?? [];
1189
+ responseWarnings = response.warnings ?? [];
1147
1190
  if (!optimizedPrompt) {
1148
1191
  return {
1149
1192
  optimizedPrompt: preprocessedPrompt,
@@ -1685,14 +1728,16 @@ function sanitizeTextOptimizationOutput(raw) {
1685
1728
  if (!normalized) {
1686
1729
  return "";
1687
1730
  }
1688
- if (!containsReasoningLeak(normalized)) {
1689
- return stripWrappingQuotes(normalized);
1731
+ let cleaned = normalized.replace(/<think>[\s\S]*?<\/think>/gi, "").replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, "").replace(/<analysis>[\s\S]*?<\/analysis>/gi, "").replace(/^(thinking|thinking process|analysis|critique|attempt|final decision|role|task|guidelines)[:=]?[\s\S]*?(?=\n\n|\n[A-Z]|$)/gim, "");
1732
+ if (!containsReasoningLeak(cleaned)) {
1733
+ return stripWrappingQuotes(cleaned);
1690
1734
  }
1691
- const candidates = raw.split(/\n{2,}/).map((chunk) => stripWrappingQuotes(normalizeWhitespace(chunk))).filter(Boolean).filter((chunk) => !containsReasoningLeak(chunk)).filter((chunk) => !/^(role|task|guidelines|thinking|thinking process|attempt|critique|final decision|analysis)\b/i.test(chunk)).filter((chunk) => !/^[-*]\s/.test(chunk)).filter((chunk) => !/^\d+\.\s/.test(chunk));
1692
- return candidates.at(-1) ?? stripWrappingQuotes(normalized);
1735
+ const candidates = cleaned.split(/\n{2,}/).map((chunk) => stripWrappingQuotes(normalizeWhitespace(chunk))).filter(Boolean).filter((chunk) => !containsReasoningLeak(chunk)).filter((chunk) => !/^(role|task|guidelines|thinking|thinking process|attempt|critique|final decision|analysis)\b/i.test(chunk)).filter((chunk) => chunk.length > 10);
1736
+ const selected = candidates.reduce((a, b) => a.length > b.length ? a : b, "");
1737
+ return selected || stripWrappingQuotes(normalized);
1693
1738
  }
1694
1739
  function containsReasoningLeak(text) {
1695
- return /(thinking process|analyze the request|drafting the optimized prompt|critique \d|attempt \d|final decision)/i.test(text);
1740
+ return /(thinking process|analyze the request|drafting the optimized prompt|critique \d|attempt \d|final decision|^thinking:|^analysis:|<think>|<reasoning>|<analysis>)/i.test(text);
1696
1741
  }
1697
1742
  function stripWrappingQuotes(text) {
1698
1743
  return text.replace(/^["'`]+|["'`]+$/g, "").trim();
@@ -1784,7 +1829,8 @@ function createOptimizer(config = {}) {
1784
1829
 
1785
1830
  // src/cliWelcome.ts
1786
1831
  import { basename } from "path";
1787
- var MIN_WIDE_COLUMNS = 84;
1832
+ var MIN_WIDE_COLUMNS = 76;
1833
+ var PANEL_RULE = "__PANEL_RULE__";
1788
1834
  function renderWelcomeScreen(options) {
1789
1835
  const columns = Math.max(60, options.columns ?? 100);
1790
1836
  const color = options.color ?? false;
@@ -1792,12 +1838,13 @@ function renderWelcomeScreen(options) {
1792
1838
  return columns >= MIN_WIDE_COLUMNS ? renderWideWelcome({ ...options, columns, color, user }) : renderCompactWelcome({ ...options, columns, color, user });
1793
1839
  }
1794
1840
  function renderWideWelcome(options) {
1795
- const width = clamp(options.columns - 5, 82, 109);
1841
+ const width = clamp(options.columns - 4, 76, 118);
1796
1842
  const innerWidth = width - 2;
1797
- const leftWidth = 28;
1843
+ const leftWidth = 30;
1798
1844
  const rightWidth = innerWidth - leftWidth - 5;
1845
+ const title = ` PromptPilot v${options.version} `;
1799
1846
  const leftLines = [
1800
- style(`Welcome back, ${options.user}`, "bold", options.color),
1847
+ style(`Welcome back ${capitalize(options.user)}!`, "bold", options.color),
1801
1848
  "",
1802
1849
  ...paintSprite(options.color),
1803
1850
  "",
@@ -1805,11 +1852,11 @@ function renderWideWelcome(options) {
1805
1852
  style(options.cwd, "dim", options.color)
1806
1853
  ];
1807
1854
  const rightLines = [
1808
- style("Launchpad", "accent", options.color),
1855
+ style("Tips for getting started", "accent", options.color),
1809
1856
  "Run " + style('promptpilot optimize "fix this CI failure" --task code --plain', "bold", options.color),
1810
1857
  "Pipe directly into Claude with " + style("| claude", "bold", options.color),
1811
- "",
1812
- style("Custom local model", "accent", options.color),
1858
+ PANEL_RULE,
1859
+ style("Local setup", "accent", options.color),
1813
1860
  "Use " + style("--model promptpilot-compressor", "bold", options.color) + " for text-only local compression",
1814
1861
  "",
1815
1862
  style("Commands", "accent", options.color),
@@ -1817,12 +1864,12 @@ function renderWideWelcome(options) {
1817
1864
  "--help show the full CLI reference"
1818
1865
  ];
1819
1866
  const rowCount = Math.max(leftLines.length, rightLines.length);
1820
- const header = `${style(" PromptPilot ", "accent", options.color)} ${style(`v${options.version}`, "dim", options.color)}`;
1821
- const topRule = `${style("\u250C", "accent", options.color)}${style("\u2500".repeat(innerWidth), "accent", options.color)}${style("\u2510", "accent", options.color)}`;
1822
- const bottomRule = `${style("\u2514", "accent", options.color)}${style("\u2500".repeat(innerWidth), "accent", options.color)}${style("\u2518", "accent", options.color)}`;
1867
+ const topRule = renderTopRule(title, innerWidth, options.color);
1868
+ const bottomRule = `${style("\u2570", "accent", options.color)}${style("\u2500".repeat(innerWidth), "accent", options.color)}${style("\u256F", "accent", options.color)}`;
1823
1869
  const body = new Array(rowCount).fill(null).map((_, index) => {
1824
1870
  const left = padVisible(leftLines[index] ?? "", leftWidth);
1825
- const right = padVisible(rightLines[index] ?? "", rightWidth);
1871
+ const rightLine = rightLines[index] ?? "";
1872
+ const right = rightLine === PANEL_RULE ? style("\u2500".repeat(rightWidth), "dim", options.color) : padVisible(rightLine, rightWidth);
1826
1873
  return `${style("\u2502", "accent", options.color)} ${left} ${style("\u2502", "accent", options.color)} ${right} ${style("\u2502", "accent", options.color)}`;
1827
1874
  });
1828
1875
  const footer = [
@@ -1830,18 +1877,20 @@ function renderWideWelcome(options) {
1830
1877
  style("Ready when you are.", "dim", options.color),
1831
1878
  `Run ${style("promptpilot --help", "bold", options.color)} for the full option list.`
1832
1879
  ];
1833
- return [header, topRule, ...body, bottomRule, ...footer].join("\n");
1880
+ return [topRule, ...body, bottomRule, ...footer].join("\n");
1834
1881
  }
1835
1882
  function renderCompactWelcome(options) {
1836
- const width = clamp(options.columns - 2, 58, 78);
1883
+ const width = clamp(options.columns - 2, 58, 82);
1837
1884
  const innerWidth = width - 2;
1885
+ const title = ` PromptPilot v${options.version} `;
1838
1886
  const lines = [
1839
- `${style("PromptPilot", "accent", options.color)} ${style(`v${options.version}`, "dim", options.color)}`,
1840
- style(`Welcome back, ${options.user}.`, "bold", options.color),
1841
- ...paintSprite(options.color),
1887
+ centerVisible(style(`Welcome back ${capitalize(options.user)}!`, "bold", options.color), innerWidth - 2),
1888
+ "",
1889
+ ...paintSprite(options.color).map((line) => centerVisible(line, innerWidth - 2)),
1890
+ "",
1842
1891
  style(options.cwd, "dim", options.color),
1843
1892
  "",
1844
- style("Quick start", "accent", options.color),
1893
+ style("Tips for getting started", "accent", options.color),
1845
1894
  'promptpilot optimize "fix this CI failure" --task code --plain',
1846
1895
  'promptpilot optimize "..." --model promptpilot-compressor',
1847
1896
  "",
@@ -1849,9 +1898,9 @@ function renderCompactWelcome(options) {
1849
1898
  "promptpilot --help"
1850
1899
  ];
1851
1900
  return [
1852
- `${style("\u250C", "accent", options.color)}${style("\u2500".repeat(innerWidth), "accent", options.color)}${style("\u2510", "accent", options.color)}`,
1853
- ...lines.map((line) => `${style("\u2502", "accent", options.color)} ${padVisible(line, innerWidth - 1)}${style("\u2502", "accent", options.color)}`),
1854
- `${style("\u2514", "accent", options.color)}${style("\u2500".repeat(innerWidth), "accent", options.color)}${style("\u2518", "accent", options.color)}`
1901
+ renderTopRule(title, innerWidth, options.color),
1902
+ ...lines.map((line) => `${style("\u2502", "accent", options.color)} ${padVisible(line, innerWidth - 2)} ${style("\u2502", "accent", options.color)}`),
1903
+ `${style("\u2570", "accent", options.color)}${style("\u2500".repeat(innerWidth), "accent", options.color)}${style("\u256F", "accent", options.color)}`
1855
1904
  ].join("\n");
1856
1905
  }
1857
1906
  function paintSprite(color) {
@@ -1881,11 +1930,24 @@ function style(text, tone, color) {
1881
1930
  return `\x1B[38;5;245m${text}\x1B[0m`;
1882
1931
  }
1883
1932
  }
1933
+ function renderTopRule(title, innerWidth, color) {
1934
+ const titleWidth = visibleWidth(title);
1935
+ const leftRuleWidth = Math.min(3, Math.max(0, innerWidth - titleWidth));
1936
+ const rightRuleWidth = Math.max(0, innerWidth - titleWidth - leftRuleWidth);
1937
+ return `${style("\u256D", "accent", color)}${style("\u2500".repeat(leftRuleWidth), "accent", color)}${style(title, "accent", color)}${style("\u2500".repeat(rightRuleWidth), "accent", color)}${style("\u256E", "accent", color)}`;
1938
+ }
1884
1939
  function padVisible(text, targetWidth) {
1885
1940
  const truncated = truncateVisible(text, targetWidth);
1886
1941
  const padding = Math.max(0, targetWidth - visibleWidth(truncated));
1887
1942
  return `${truncated}${" ".repeat(padding)}`;
1888
1943
  }
1944
+ function centerVisible(text, targetWidth) {
1945
+ const truncated = truncateVisible(text, targetWidth);
1946
+ const extra = Math.max(0, targetWidth - visibleWidth(truncated));
1947
+ const leftPadding = Math.floor(extra / 2);
1948
+ const rightPadding = extra - leftPadding;
1949
+ return `${" ".repeat(leftPadding)}${truncated}${" ".repeat(rightPadding)}`;
1950
+ }
1889
1951
  function truncateVisible(text, targetWidth) {
1890
1952
  if (visibleWidth(text) <= targetWidth) {
1891
1953
  return text;
@@ -1918,6 +1980,50 @@ function visibleWidth(text) {
1918
1980
  function clamp(value, min, max) {
1919
1981
  return Math.max(min, Math.min(max, value));
1920
1982
  }
1983
+ function capitalize(value) {
1984
+ if (value.length === 0) {
1985
+ return value;
1986
+ }
1987
+ return value[0].toUpperCase() + value.slice(1);
1988
+ }
1989
+
1990
+ // src/utils/spinner.ts
1991
+ var Spinner = class {
1992
+ message = "";
1993
+ frame = 0;
1994
+ interval = null;
1995
+ writer;
1996
+ isTTY;
1997
+ frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1998
+ constructor(writer, isTTY = false) {
1999
+ this.writer = writer;
2000
+ this.isTTY = isTTY;
2001
+ }
2002
+ start(message) {
2003
+ if (!this.isTTY) {
2004
+ return;
2005
+ }
2006
+ this.message = message;
2007
+ this.frame = 0;
2008
+ this.interval = setInterval(() => {
2009
+ const spinner = this.frames[this.frame % this.frames.length];
2010
+ this.writer.write(`\r${spinner} ${this.message}`);
2011
+ this.frame += 1;
2012
+ }, 80);
2013
+ }
2014
+ stop() {
2015
+ if (this.interval) {
2016
+ clearInterval(this.interval);
2017
+ this.interval = null;
2018
+ }
2019
+ if (this.isTTY) {
2020
+ this.writer.write("\r\x1B[K");
2021
+ }
2022
+ }
2023
+ };
2024
+ function createSpinner(writer, isTTY = false) {
2025
+ return new Spinner(writer, isTTY);
2026
+ }
1921
2027
 
1922
2028
  // src/cli.ts
1923
2029
  async function runCli(argv, io = { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin }, dependencies = { createOptimizer, readStdin, getCliInfo }) {
@@ -1981,7 +2087,9 @@ async function runCli(argv, io = { stdout: process.stdout, stderr: process.stder
1981
2087
  io.stderr.write("A prompt is required.\n");
1982
2088
  return 1;
1983
2089
  }
2090
+ const spinner = createSpinner(io.stderr, io.stderr.isTTY ?? false);
1984
2091
  try {
2092
+ spinner.start("optimizing");
1985
2093
  const result = await optimizer.optimize({
1986
2094
  prompt: parsed.prompt,
1987
2095
  task: parsed.task,
@@ -2010,11 +2118,26 @@ async function runCli(argv, io = { stdout: process.stdout, stderr: process.stder
2010
2118
  timeoutMs: parsed.timeoutMs,
2011
2119
  bypassOptimization: parsed.bypassOptimization
2012
2120
  });
2121
+ spinner.stop();
2013
2122
  if (parsed.json) {
2014
2123
  io.stdout.write(`${toPrettyJson(result)}
2015
2124
  `);
2016
2125
  return 0;
2017
2126
  }
2127
+ if (parsed.clipboard) {
2128
+ const copied = copyToClipboard(result.finalPrompt);
2129
+ if (copied) {
2130
+ io.stderr.write(`\u2713 Copied optimized prompt to clipboard
2131
+ `);
2132
+ return 0;
2133
+ } else {
2134
+ io.stderr.write(`\u2717 Failed to copy to clipboard. Install xclip, xsel, or wl-copy.
2135
+ `);
2136
+ io.stdout.write(`${result.finalPrompt}
2137
+ `);
2138
+ return 1;
2139
+ }
2140
+ }
2018
2141
  if (parsed.plain) {
2019
2142
  io.stdout.write(`${result.finalPrompt}
2020
2143
  `);
@@ -2024,6 +2147,8 @@ async function runCli(argv, io = { stdout: process.stdout, stderr: process.stder
2024
2147
 
2025
2148
  `);
2026
2149
  io.stdout.write(`provider=${result.provider} model=${result.model} tokens=${result.estimatedTokensAfter.total} savings=${result.tokenSavings}
2150
+ `);
2151
+ io.stdout.write(` prompt_savings=${result.promptCompressionSavings} context_savings=${result.contextCompressionSavings} wrapper_overhead=${result.wrapperOverhead}
2027
2152
  `);
2028
2153
  if (result.selectedTarget) {
2029
2154
  io.stdout.write(`selected_target=${formatTarget(result.selectedTarget)}
@@ -2035,6 +2160,7 @@ async function runCli(argv, io = { stdout: process.stdout, stderr: process.stder
2035
2160
  }
2036
2161
  return 0;
2037
2162
  } catch (error) {
2163
+ spinner.stop();
2038
2164
  const message = error instanceof Error ? error.message : "Unknown CLI error.";
2039
2165
  io.stderr.write(`${message}
2040
2166
  `);
@@ -2046,6 +2172,7 @@ function parseOptimizeArgs(args) {
2046
2172
  plain: false,
2047
2173
  json: false,
2048
2174
  debug: false,
2175
+ clipboard: false,
2049
2176
  clearSession: false,
2050
2177
  useContext: true,
2051
2178
  bypassOptimization: false,
@@ -2129,6 +2256,9 @@ function parseOptimizeArgs(args) {
2129
2256
  case "--json":
2130
2257
  parsed.json = true;
2131
2258
  break;
2259
+ case "--clipboard":
2260
+ parsed.clipboard = true;
2261
+ break;
2132
2262
  case "--debug":
2133
2263
  parsed.debug = true;
2134
2264
  break;
@@ -2195,6 +2325,7 @@ function getHelpText() {
2195
2325
  " --sqlite-path <path>",
2196
2326
  " --plain",
2197
2327
  " --json",
2328
+ " --clipboard Copy optimized prompt to clipboard",
2198
2329
  " --debug",
2199
2330
  " --save-context",
2200
2331
  " --no-context",
@@ -2264,6 +2395,23 @@ function readPackageVersion() {
2264
2395
  return "dev";
2265
2396
  }
2266
2397
  }
2398
+ function copyToClipboard(text) {
2399
+ const commands = [
2400
+ { cmd: "xclip", args: ["-selection", "clipboard"], platform: "linux" },
2401
+ { cmd: "xsel", args: ["-b"], platform: "linux" },
2402
+ { cmd: "wl-copy", args: [], platform: "linux" },
2403
+ { cmd: "pbcopy", args: [], platform: "darwin" }
2404
+ ];
2405
+ for (const { cmd } of commands) {
2406
+ try {
2407
+ execSync(`which ${cmd} > /dev/null 2>&1`);
2408
+ execSync(cmd, { input: text, stdio: ["pipe", "ignore", "ignore"] });
2409
+ return true;
2410
+ } catch {
2411
+ }
2412
+ }
2413
+ return false;
2414
+ }
2267
2415
  if (isMainModule()) {
2268
2416
  runCli(process.argv.slice(2)).then(
2269
2417
  (code) => {