@oh-my-pi/pi-coding-agent 3.25.0 → 3.31.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.
- package/CHANGELOG.md +90 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +369 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/complete.ts +2 -4
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +6 -8
- package/src/core/tools/jtd-to-json-schema.ts +174 -196
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +58 -9
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +55 -32
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +152 -76
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/executor.ts +204 -67
- package/src/core/tools/task/index.ts +129 -92
- package/src/core/tools/task/name-generator.ts +1544 -214
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +34 -11
- package/src/core/tools/task/worker.ts +152 -27
- package/src/core/tools/web-fetch.ts +220 -1657
- package/src/core/tools/web-scrapers/academic.test.ts +239 -0
- package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
- package/src/core/tools/web-scrapers/arxiv.ts +88 -0
- package/src/core/tools/web-scrapers/aur.ts +175 -0
- package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
- package/src/core/tools/web-scrapers/bluesky.ts +284 -0
- package/src/core/tools/web-scrapers/brew.ts +177 -0
- package/src/core/tools/web-scrapers/business.test.ts +82 -0
- package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
- package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/web-scrapers/coingecko.ts +184 -0
- package/src/core/tools/web-scrapers/crates-io.ts +128 -0
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-scrapers/devto.ts +177 -0
- package/src/core/tools/web-scrapers/discogs.ts +308 -0
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
- package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/finance-media.test.ts +144 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/web-scrapers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-scrapers/github-gist.ts +68 -0
- package/src/core/tools/web-scrapers/github.ts +455 -0
- package/src/core/tools/web-scrapers/gitlab.ts +456 -0
- package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
- package/src/core/tools/web-scrapers/hackage.ts +94 -0
- package/src/core/tools/web-scrapers/hackernews.ts +208 -0
- package/src/core/tools/web-scrapers/hex.ts +121 -0
- package/src/core/tools/web-scrapers/huggingface.ts +385 -0
- package/src/core/tools/web-scrapers/iacr.ts +86 -0
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/web-scrapers/lobsters.ts +186 -0
- package/src/core/tools/web-scrapers/mastodon.ts +310 -0
- package/src/core/tools/web-scrapers/maven.ts +152 -0
- package/src/core/tools/web-scrapers/mdn.ts +174 -0
- package/src/core/tools/web-scrapers/media.test.ts +138 -0
- package/src/core/tools/web-scrapers/metacpan.ts +253 -0
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/web-scrapers/npm.ts +114 -0
- package/src/core/tools/web-scrapers/nuget.ts +205 -0
- package/src/core/tools/web-scrapers/nvd.ts +243 -0
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/web-scrapers/opencorporates.ts +275 -0
- package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/web-scrapers/osv.ts +189 -0
- package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
- package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
- package/src/core/tools/web-scrapers/packagist.ts +174 -0
- package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
- package/src/core/tools/web-scrapers/pubmed.ts +178 -0
- package/src/core/tools/web-scrapers/pypi.ts +129 -0
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
- package/src/core/tools/web-scrapers/reddit.ts +104 -0
- package/src/core/tools/web-scrapers/repology.ts +262 -0
- package/src/core/tools/web-scrapers/research.test.ts +107 -0
- package/src/core/tools/web-scrapers/rfc.ts +209 -0
- package/src/core/tools/web-scrapers/rubygems.ts +117 -0
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
- package/src/core/tools/web-scrapers/security.test.ts +103 -0
- package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
- package/src/core/tools/web-scrapers/social.test.ts +259 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/web-scrapers/spotify.ts +218 -0
- package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
- package/src/core/tools/web-scrapers/standards.test.ts +122 -0
- package/src/core/tools/web-scrapers/terraform.ts +304 -0
- package/src/core/tools/web-scrapers/tldr.ts +51 -0
- package/src/core/tools/web-scrapers/twitter.ts +96 -0
- package/src/core/tools/web-scrapers/types.ts +234 -0
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/web-scrapers/vimeo.ts +152 -0
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/web-scrapers/wikidata.ts +357 -0
- package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
- package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
- package/src/core/tools/web-scrapers/youtube.ts +371 -0
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -38
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/src/utils/tools-manager.ts +110 -8
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface BrewFormula {
|
|
5
|
+
name: string;
|
|
6
|
+
full_name?: string;
|
|
7
|
+
desc?: string;
|
|
8
|
+
homepage?: string;
|
|
9
|
+
license?: string;
|
|
10
|
+
versions?: {
|
|
11
|
+
stable?: string;
|
|
12
|
+
head?: string;
|
|
13
|
+
bottle?: boolean;
|
|
14
|
+
};
|
|
15
|
+
dependencies?: string[];
|
|
16
|
+
build_dependencies?: string[];
|
|
17
|
+
optional_dependencies?: string[];
|
|
18
|
+
conflicts_with?: string[];
|
|
19
|
+
caveats?: string;
|
|
20
|
+
analytics?: {
|
|
21
|
+
install?: {
|
|
22
|
+
"30d"?: Record<string, number>;
|
|
23
|
+
"90d"?: Record<string, number>;
|
|
24
|
+
"365d"?: Record<string, number>;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface BrewCask {
|
|
30
|
+
token: string;
|
|
31
|
+
name?: string[];
|
|
32
|
+
desc?: string;
|
|
33
|
+
homepage?: string;
|
|
34
|
+
version?: string;
|
|
35
|
+
sha256?: string;
|
|
36
|
+
caveats?: string;
|
|
37
|
+
depends_on?: {
|
|
38
|
+
macos?: Record<string, string[]>;
|
|
39
|
+
};
|
|
40
|
+
conflicts_with?: {
|
|
41
|
+
cask?: string[];
|
|
42
|
+
};
|
|
43
|
+
analytics?: {
|
|
44
|
+
install?: {
|
|
45
|
+
"30d"?: Record<string, number>;
|
|
46
|
+
"90d"?: Record<string, number>;
|
|
47
|
+
"365d"?: Record<string, number>;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getInstallCount(analytics?: { install?: { "30d"?: Record<string, number> } }): number | null {
|
|
53
|
+
if (!analytics?.install?.["30d"]) return null;
|
|
54
|
+
const counts = Object.values(analytics.install["30d"]);
|
|
55
|
+
return counts.reduce((sum, n) => sum + n, 0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handle Homebrew formulae and cask URLs via API
|
|
60
|
+
*/
|
|
61
|
+
export const handleBrew: SpecialHandler = async (
|
|
62
|
+
url: string,
|
|
63
|
+
timeout: number,
|
|
64
|
+
signal?: AbortSignal,
|
|
65
|
+
): Promise<RenderResult | null> => {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = new URL(url);
|
|
68
|
+
if (parsed.hostname !== "formulae.brew.sh") return null;
|
|
69
|
+
|
|
70
|
+
const formulaMatch = parsed.pathname.match(/^\/formula\/([^/]+)\/?$/);
|
|
71
|
+
const caskMatch = parsed.pathname.match(/^\/cask\/([^/]+)\/?$/);
|
|
72
|
+
|
|
73
|
+
if (!formulaMatch && !caskMatch) return null;
|
|
74
|
+
|
|
75
|
+
const fetchedAt = new Date().toISOString();
|
|
76
|
+
const isFormula = Boolean(formulaMatch);
|
|
77
|
+
const name = decodeURIComponent(isFormula ? formulaMatch![1] : caskMatch![1]);
|
|
78
|
+
|
|
79
|
+
const apiUrl = isFormula
|
|
80
|
+
? `https://formulae.brew.sh/api/formula/${encodeURIComponent(name)}.json`
|
|
81
|
+
: `https://formulae.brew.sh/api/cask/${encodeURIComponent(name)}.json`;
|
|
82
|
+
|
|
83
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
84
|
+
if (!result.ok) return null;
|
|
85
|
+
|
|
86
|
+
let md: string;
|
|
87
|
+
|
|
88
|
+
if (isFormula) {
|
|
89
|
+
const formula: BrewFormula = JSON.parse(result.content);
|
|
90
|
+
|
|
91
|
+
md = `# ${formula.full_name || formula.name}\n\n`;
|
|
92
|
+
if (formula.desc) md += `${formula.desc}\n\n`;
|
|
93
|
+
|
|
94
|
+
md += `**Version:** ${formula.versions?.stable || "unknown"}`;
|
|
95
|
+
if (formula.license) md += ` · **License:** ${formula.license}`;
|
|
96
|
+
md += "\n";
|
|
97
|
+
|
|
98
|
+
const installs = getInstallCount(formula.analytics);
|
|
99
|
+
if (installs !== null) {
|
|
100
|
+
md += `**Installs (30d):** ${formatCount(installs)}\n`;
|
|
101
|
+
}
|
|
102
|
+
md += "\n";
|
|
103
|
+
|
|
104
|
+
md += `\`\`\`bash\nbrew install ${formula.name}\n\`\`\`\n\n`;
|
|
105
|
+
|
|
106
|
+
if (formula.homepage) md += `**Homepage:** ${formula.homepage}\n`;
|
|
107
|
+
|
|
108
|
+
if (formula.dependencies?.length) {
|
|
109
|
+
md += `\n## Dependencies\n\n`;
|
|
110
|
+
for (const dep of formula.dependencies) {
|
|
111
|
+
md += `- ${dep}\n`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (formula.build_dependencies?.length) {
|
|
116
|
+
md += `\n## Build Dependencies\n\n`;
|
|
117
|
+
for (const dep of formula.build_dependencies) {
|
|
118
|
+
md += `- ${dep}\n`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (formula.conflicts_with?.length) {
|
|
123
|
+
md += `\n## Conflicts With\n\n`;
|
|
124
|
+
for (const conflict of formula.conflicts_with) {
|
|
125
|
+
md += `- ${conflict}\n`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (formula.caveats) {
|
|
130
|
+
md += `\n## Caveats\n\n${formula.caveats}\n`;
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
const cask: BrewCask = JSON.parse(result.content);
|
|
134
|
+
|
|
135
|
+
const displayName = cask.name?.[0] || cask.token;
|
|
136
|
+
md = `# ${displayName}\n\n`;
|
|
137
|
+
if (cask.desc) md += `${cask.desc}\n\n`;
|
|
138
|
+
|
|
139
|
+
md += `**Version:** ${cask.version || "unknown"}\n`;
|
|
140
|
+
|
|
141
|
+
const installs = getInstallCount(cask.analytics);
|
|
142
|
+
if (installs !== null) {
|
|
143
|
+
md += `**Installs (30d):** ${formatCount(installs)}\n`;
|
|
144
|
+
}
|
|
145
|
+
md += "\n";
|
|
146
|
+
|
|
147
|
+
md += `\`\`\`bash\nbrew install --cask ${cask.token}\n\`\`\`\n\n`;
|
|
148
|
+
|
|
149
|
+
if (cask.homepage) md += `**Homepage:** ${cask.homepage}\n`;
|
|
150
|
+
|
|
151
|
+
if (cask.conflicts_with?.cask?.length) {
|
|
152
|
+
md += `\n## Conflicts With\n\n`;
|
|
153
|
+
for (const conflict of cask.conflicts_with.cask) {
|
|
154
|
+
md += `- ${conflict}\n`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (cask.caveats) {
|
|
159
|
+
md += `\n## Caveats\n\n${cask.caveats}\n`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const output = finalizeOutput(md);
|
|
164
|
+
return {
|
|
165
|
+
url,
|
|
166
|
+
finalUrl: url,
|
|
167
|
+
contentType: "text/markdown",
|
|
168
|
+
method: "brew",
|
|
169
|
+
content: output.content,
|
|
170
|
+
fetchedAt,
|
|
171
|
+
truncated: output.truncated,
|
|
172
|
+
notes: [`Fetched via Homebrew ${isFormula ? "formula" : "cask"} API`],
|
|
173
|
+
};
|
|
174
|
+
} catch {}
|
|
175
|
+
|
|
176
|
+
return null;
|
|
177
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { handleOpenCorporates } from "./opencorporates";
|
|
3
|
+
import { handleSecEdgar } from "./sec-edgar";
|
|
4
|
+
|
|
5
|
+
const SKIP = !process.env.WEB_FETCH_INTEGRATION;
|
|
6
|
+
|
|
7
|
+
describe.skipIf(SKIP)("handleSecEdgar", () => {
|
|
8
|
+
it("returns null for non-matching URLs", async () => {
|
|
9
|
+
const result = await handleSecEdgar("https://example.com", 20);
|
|
10
|
+
expect(result).toBeNull();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns null for SEC URLs without valid CIK", async () => {
|
|
14
|
+
const result = await handleSecEdgar("https://www.sec.gov/about.html", 20);
|
|
15
|
+
expect(result).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("fetches Apple Inc filings by CIK query param", async () => {
|
|
19
|
+
const result = await handleSecEdgar(
|
|
20
|
+
"https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=0000320193",
|
|
21
|
+
20,
|
|
22
|
+
);
|
|
23
|
+
expect(result).not.toBeNull();
|
|
24
|
+
expect(result?.method).toBe("sec-edgar");
|
|
25
|
+
expect(result?.content).toContain("APPLE INC");
|
|
26
|
+
expect(result?.content).toContain("0000320193");
|
|
27
|
+
expect(result?.content).toContain("10-K"); // Apple files 10-K annually
|
|
28
|
+
expect(result?.contentType).toBe("text/markdown");
|
|
29
|
+
expect(result?.fetchedAt).toBeTruthy();
|
|
30
|
+
expect(result?.truncated).toBeDefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("fetches via data.sec.gov submissions URL", async () => {
|
|
34
|
+
const result = await handleSecEdgar("https://data.sec.gov/submissions/CIK0000320193.json", 20);
|
|
35
|
+
expect(result).not.toBeNull();
|
|
36
|
+
expect(result?.method).toBe("sec-edgar");
|
|
37
|
+
expect(result?.content).toContain("APPLE INC");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("fetches via Archives path", async () => {
|
|
41
|
+
// Any filing URL with CIK in Archives path
|
|
42
|
+
const result = await handleSecEdgar(
|
|
43
|
+
"https://www.sec.gov/Archives/edgar/data/320193/000032019324000123/aapl-20240928.htm",
|
|
44
|
+
20,
|
|
45
|
+
);
|
|
46
|
+
expect(result).not.toBeNull();
|
|
47
|
+
expect(result?.method).toBe("sec-edgar");
|
|
48
|
+
expect(result?.content).toContain("APPLE INC");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe.skipIf(SKIP)("handleOpenCorporates", () => {
|
|
53
|
+
it("returns null for non-matching URLs", async () => {
|
|
54
|
+
const result = await handleOpenCorporates("https://example.com", 20);
|
|
55
|
+
expect(result).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns null for OpenCorporates URLs without company path", async () => {
|
|
59
|
+
const result = await handleOpenCorporates("https://opencorporates.com/about", 20);
|
|
60
|
+
expect(result).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("fetches Apple Inc Delaware registration", async () => {
|
|
64
|
+
const result = await handleOpenCorporates("https://opencorporates.com/companies/us_de/2927442", 20);
|
|
65
|
+
expect(result).not.toBeNull();
|
|
66
|
+
expect(result?.method).toBe("opencorporates");
|
|
67
|
+
expect(result?.content).toContain("APPLE INC");
|
|
68
|
+
expect(result?.content).toContain("2927442");
|
|
69
|
+
expect(result?.content).toContain("US_DE");
|
|
70
|
+
expect(result?.contentType).toBe("text/markdown");
|
|
71
|
+
expect(result?.fetchedAt).toBeTruthy();
|
|
72
|
+
expect(result?.truncated).toBeDefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("fetches Microsoft Corporation", async () => {
|
|
76
|
+
// Microsoft is registered in Washington state
|
|
77
|
+
const result = await handleOpenCorporates("https://opencorporates.com/companies/us_wa/600413485", 20);
|
|
78
|
+
expect(result).not.toBeNull();
|
|
79
|
+
expect(result?.method).toBe("opencorporates");
|
|
80
|
+
expect(result?.content).toMatch(/microsoft/i);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handle cheat.sh / cht.sh URLs for command cheatsheets
|
|
6
|
+
*
|
|
7
|
+
* API: Plain text at https://cheat.sh/{topic}?T (T flag removes ANSI colors)
|
|
8
|
+
* Supports: commands, language/topic queries (e.g., python/list, go/slice)
|
|
9
|
+
*/
|
|
10
|
+
export const handleCheatSh: SpecialHandler = async (
|
|
11
|
+
url: string,
|
|
12
|
+
timeout: number,
|
|
13
|
+
signal?: AbortSignal,
|
|
14
|
+
): Promise<RenderResult | null> => {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = new URL(url);
|
|
17
|
+
if (parsed.hostname !== "cheat.sh" && parsed.hostname !== "cht.sh") return null;
|
|
18
|
+
|
|
19
|
+
// Extract topic from path (everything after /)
|
|
20
|
+
const topic = parsed.pathname.slice(1);
|
|
21
|
+
if (!topic || topic === "" || topic === "/") return null;
|
|
22
|
+
|
|
23
|
+
const fetchedAt = new Date().toISOString();
|
|
24
|
+
|
|
25
|
+
// Fetch from cheat.sh API with ?T to disable ANSI colors
|
|
26
|
+
const apiUrl = `https://cheat.sh/${encodeURIComponent(topic)}?T`;
|
|
27
|
+
const result = await loadPage(apiUrl, {
|
|
28
|
+
timeout,
|
|
29
|
+
signal,
|
|
30
|
+
headers: {
|
|
31
|
+
Accept: "text/plain",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!result.ok || !result.content.trim()) return null;
|
|
36
|
+
|
|
37
|
+
// Format the cheatsheet as markdown
|
|
38
|
+
const decodedTopic = decodeURIComponent(topic);
|
|
39
|
+
let md = `# cheat.sh/${decodedTopic}\n\n`;
|
|
40
|
+
|
|
41
|
+
// The content is already plain text, wrap in code block for readability
|
|
42
|
+
// Detect if it looks like code/commands vs prose
|
|
43
|
+
const content = result.content.trim();
|
|
44
|
+
const lines = content.split("\n");
|
|
45
|
+
const hasCodeIndicators = lines.some(
|
|
46
|
+
(line) =>
|
|
47
|
+
line.startsWith("$") ||
|
|
48
|
+
line.startsWith("#") ||
|
|
49
|
+
line.includes("()") ||
|
|
50
|
+
line.includes("=>") ||
|
|
51
|
+
/^\s*(if|for|while|def|func|fn|let|const|var)\b/.test(line),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (hasCodeIndicators || decodedTopic.includes("/")) {
|
|
55
|
+
// Likely code examples - use code block
|
|
56
|
+
// Try to detect language from topic
|
|
57
|
+
const lang = decodedTopic.split("/")[0] || "bash";
|
|
58
|
+
md += `\`\`\`${lang}\n${content}\n\`\`\`\n`;
|
|
59
|
+
} else {
|
|
60
|
+
// Command cheatsheet - format as-is (already has good structure)
|
|
61
|
+
md += `\`\`\`\n${content}\n\`\`\`\n`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const output = finalizeOutput(md);
|
|
65
|
+
return {
|
|
66
|
+
url,
|
|
67
|
+
finalUrl: url,
|
|
68
|
+
contentType: "text/markdown",
|
|
69
|
+
method: "cheat.sh",
|
|
70
|
+
content: output.content,
|
|
71
|
+
fetchedAt,
|
|
72
|
+
truncated: output.truncated,
|
|
73
|
+
notes: ["Fetched via cheat.sh"],
|
|
74
|
+
};
|
|
75
|
+
} catch {}
|
|
76
|
+
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, formatCount, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface NuGetODataEntry {
|
|
5
|
+
Id: string;
|
|
6
|
+
Version: string;
|
|
7
|
+
Title?: string;
|
|
8
|
+
Description?: string;
|
|
9
|
+
Summary?: string;
|
|
10
|
+
Authors?: string;
|
|
11
|
+
ProjectUrl?: string;
|
|
12
|
+
PackageSourceUrl?: string;
|
|
13
|
+
Tags?: string;
|
|
14
|
+
DownloadCount?: number;
|
|
15
|
+
VersionDownloadCount?: number;
|
|
16
|
+
Published?: string;
|
|
17
|
+
LicenseUrl?: string;
|
|
18
|
+
ReleaseNotes?: string;
|
|
19
|
+
Dependencies?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface NuGetODataResponse {
|
|
23
|
+
d?: {
|
|
24
|
+
results?: NuGetODataEntry[];
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handle Chocolatey package URLs via NuGet v2 OData API
|
|
30
|
+
*/
|
|
31
|
+
export const handleChocolatey: SpecialHandler = async (
|
|
32
|
+
url: string,
|
|
33
|
+
timeout: number,
|
|
34
|
+
signal?: AbortSignal,
|
|
35
|
+
): Promise<RenderResult | null> => {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(url);
|
|
38
|
+
if (!parsed.hostname.includes("chocolatey.org")) return null;
|
|
39
|
+
|
|
40
|
+
// Extract package name from /packages/{name} or /packages/{name}/{version}
|
|
41
|
+
const match = parsed.pathname.match(/^\/packages\/([^/]+)(?:\/([^/]+))?/);
|
|
42
|
+
if (!match) return null;
|
|
43
|
+
|
|
44
|
+
const packageName = decodeURIComponent(match[1]);
|
|
45
|
+
const specificVersion = match[2] ? decodeURIComponent(match[2]) : null;
|
|
46
|
+
|
|
47
|
+
const fetchedAt = new Date().toISOString();
|
|
48
|
+
|
|
49
|
+
// Build OData query - filter by Id and optionally version
|
|
50
|
+
let apiUrl = `https://community.chocolatey.org/api/v2/Packages()?$filter=Id%20eq%20'${encodeURIComponent(packageName)}'`;
|
|
51
|
+
if (specificVersion) {
|
|
52
|
+
apiUrl += `%20and%20Version%20eq%20'${encodeURIComponent(specificVersion)}'`;
|
|
53
|
+
} else {
|
|
54
|
+
// Get latest version by ordering and taking first
|
|
55
|
+
apiUrl += "&$orderby=Version%20desc&$top=1";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = await loadPage(apiUrl, {
|
|
59
|
+
timeout,
|
|
60
|
+
signal,
|
|
61
|
+
headers: {
|
|
62
|
+
Accept: "application/json",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!result.ok) return null;
|
|
67
|
+
|
|
68
|
+
let data: NuGetODataResponse;
|
|
69
|
+
try {
|
|
70
|
+
data = JSON.parse(result.content);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const pkg = data.d?.results?.[0];
|
|
76
|
+
if (!pkg) return null;
|
|
77
|
+
|
|
78
|
+
// Build markdown output
|
|
79
|
+
let md = `# ${pkg.Title || pkg.Id}\n\n`;
|
|
80
|
+
|
|
81
|
+
if (pkg.Summary) {
|
|
82
|
+
md += `${pkg.Summary}\n\n`;
|
|
83
|
+
} else if (pkg.Description) {
|
|
84
|
+
// Use first paragraph of description as summary
|
|
85
|
+
const firstPara = pkg.Description.split(/\n\n/)[0];
|
|
86
|
+
md += `${firstPara}\n\n`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
md += `**Version:** ${pkg.Version}`;
|
|
90
|
+
if (pkg.Authors) md += ` · **Authors:** ${pkg.Authors}`;
|
|
91
|
+
md += "\n";
|
|
92
|
+
|
|
93
|
+
if (pkg.DownloadCount !== undefined) {
|
|
94
|
+
md += `**Total Downloads:** ${formatCount(pkg.DownloadCount)}`;
|
|
95
|
+
if (pkg.VersionDownloadCount !== undefined) {
|
|
96
|
+
md += ` · **Version Downloads:** ${formatCount(pkg.VersionDownloadCount)}`;
|
|
97
|
+
}
|
|
98
|
+
md += "\n";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (pkg.Published) {
|
|
102
|
+
const date = new Date(pkg.Published);
|
|
103
|
+
md += `**Published:** ${date.toISOString().split("T")[0]}\n`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
md += "\n";
|
|
107
|
+
|
|
108
|
+
if (pkg.ProjectUrl) md += `**Project URL:** ${pkg.ProjectUrl}\n`;
|
|
109
|
+
if (pkg.PackageSourceUrl) md += `**Source:** ${pkg.PackageSourceUrl}\n`;
|
|
110
|
+
if (pkg.LicenseUrl) md += `**License:** ${pkg.LicenseUrl}\n`;
|
|
111
|
+
|
|
112
|
+
if (pkg.Tags) {
|
|
113
|
+
const tags = pkg.Tags.split(/\s+/).filter((t) => t.length > 0);
|
|
114
|
+
if (tags.length > 0) {
|
|
115
|
+
md += `**Tags:** ${tags.join(", ")}\n`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Full description if different from summary
|
|
120
|
+
if (pkg.Description && pkg.Description !== pkg.Summary) {
|
|
121
|
+
md += `\n## Description\n\n${pkg.Description}\n`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (pkg.ReleaseNotes) {
|
|
125
|
+
md += `\n## Release Notes\n\n${pkg.ReleaseNotes}\n`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (pkg.Dependencies) {
|
|
129
|
+
// Dependencies format: "id:version|id:version"
|
|
130
|
+
const deps = pkg.Dependencies.split("|").filter((d) => d.trim().length > 0);
|
|
131
|
+
if (deps.length > 0) {
|
|
132
|
+
md += `\n## Dependencies\n\n`;
|
|
133
|
+
for (const dep of deps) {
|
|
134
|
+
const [depId, depVersion] = dep.split(":");
|
|
135
|
+
if (depId) {
|
|
136
|
+
md += `- ${depId}${depVersion ? `: ${depVersion}` : ""}\n`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
md += `\n---\n**Install:** \`choco install ${packageName}\`\n`;
|
|
143
|
+
|
|
144
|
+
const output = finalizeOutput(md);
|
|
145
|
+
return {
|
|
146
|
+
url,
|
|
147
|
+
finalUrl: url,
|
|
148
|
+
contentType: "text/markdown",
|
|
149
|
+
method: "chocolatey",
|
|
150
|
+
content: output.content,
|
|
151
|
+
fetchedAt,
|
|
152
|
+
truncated: output.truncated,
|
|
153
|
+
notes: ["Fetched via Chocolatey NuGet API"],
|
|
154
|
+
};
|
|
155
|
+
} catch {}
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { parseFrontmatter } from "../../../discovery/helpers";
|
|
2
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
4
|
+
|
|
5
|
+
const ALLOWED_HOSTS = new Set(["choosealicense.com", "www.choosealicense.com"]);
|
|
6
|
+
const LICENSE_PATH = /^\/licenses\/([^/]+)\/?$/i;
|
|
7
|
+
const APPENDIX_PATH = /^\/appendix\/?$/i;
|
|
8
|
+
|
|
9
|
+
function asString(value: unknown): string | undefined {
|
|
10
|
+
if (typeof value !== "string") return undefined;
|
|
11
|
+
const trimmed = value.trim();
|
|
12
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeList(value: unknown): string[] {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return value
|
|
18
|
+
.filter((item): item is string => typeof item === "string")
|
|
19
|
+
.map((item) => item.trim())
|
|
20
|
+
.filter((item) => item.length > 0);
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "string") {
|
|
23
|
+
return value
|
|
24
|
+
.split(",")
|
|
25
|
+
.map((item) => item.trim())
|
|
26
|
+
.filter((item) => item.length > 0);
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatLabel(value: string): string {
|
|
32
|
+
const cleaned = value.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
|
|
33
|
+
if (!cleaned) return value;
|
|
34
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatSection(title: string, items: string[]): string {
|
|
38
|
+
let md = `## ${title}\n\n`;
|
|
39
|
+
if (items.length === 0) {
|
|
40
|
+
md += "- None listed\n\n";
|
|
41
|
+
return md;
|
|
42
|
+
}
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
md += `- ${formatLabel(item)}\n`;
|
|
45
|
+
}
|
|
46
|
+
md += "\n";
|
|
47
|
+
return md;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const handleChooseALicense: SpecialHandler = async (
|
|
51
|
+
url: string,
|
|
52
|
+
timeout: number,
|
|
53
|
+
signal?: AbortSignal,
|
|
54
|
+
): Promise<RenderResult | null> => {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new URL(url);
|
|
57
|
+
if (!ALLOWED_HOSTS.has(parsed.hostname)) return null;
|
|
58
|
+
|
|
59
|
+
const licenseMatch = parsed.pathname.match(LICENSE_PATH);
|
|
60
|
+
const isAppendix = APPENDIX_PATH.test(parsed.pathname);
|
|
61
|
+
if (!licenseMatch && !isAppendix) return null;
|
|
62
|
+
|
|
63
|
+
const licenseSlug = licenseMatch ? decodeURIComponent(licenseMatch[1]).toLowerCase() : "appendix";
|
|
64
|
+
const rawUrl = licenseMatch
|
|
65
|
+
? `https://raw.githubusercontent.com/github/choosealicense.com/gh-pages/_licenses/${licenseSlug}.txt`
|
|
66
|
+
: "https://raw.githubusercontent.com/github/choosealicense.com/gh-pages/_pages/appendix.md";
|
|
67
|
+
|
|
68
|
+
const fetchedAt = new Date().toISOString();
|
|
69
|
+
const result = await loadPage(rawUrl, { timeout, headers: { Accept: "text/plain" }, signal });
|
|
70
|
+
if (!result.ok) return null;
|
|
71
|
+
|
|
72
|
+
const { frontmatter, body } = parseFrontmatter(result.content);
|
|
73
|
+
|
|
74
|
+
const title = asString(frontmatter.title) ?? formatLabel(licenseSlug);
|
|
75
|
+
const spdxId = asString(frontmatter["spdx-id"]) ?? "Unknown";
|
|
76
|
+
const description = asString(frontmatter.description);
|
|
77
|
+
const permissions = normalizeList(frontmatter.permissions);
|
|
78
|
+
const conditions = normalizeList(frontmatter.conditions);
|
|
79
|
+
const limitations = normalizeList(frontmatter.limitations);
|
|
80
|
+
|
|
81
|
+
let md = `# ${title}\n\n`;
|
|
82
|
+
if (description) md += `${description}\n\n`;
|
|
83
|
+
|
|
84
|
+
md += `**SPDX ID:** ${spdxId}\n`;
|
|
85
|
+
md += `**Source:** https://choosealicense.com${isAppendix ? "/appendix" : `/licenses/${licenseSlug}/`}\n\n`;
|
|
86
|
+
|
|
87
|
+
md += formatSection("Permissions", permissions);
|
|
88
|
+
md += formatSection("Conditions", conditions);
|
|
89
|
+
md += formatSection("Limitations", limitations);
|
|
90
|
+
|
|
91
|
+
const licenseText = body.trim();
|
|
92
|
+
if (licenseText.length > 0) {
|
|
93
|
+
md += `---\n\n## License Text\n\n${licenseText}\n`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const output = finalizeOutput(md);
|
|
97
|
+
return {
|
|
98
|
+
url,
|
|
99
|
+
finalUrl: url,
|
|
100
|
+
contentType: "text/markdown",
|
|
101
|
+
method: "choosealicense",
|
|
102
|
+
content: output.content,
|
|
103
|
+
fetchedAt,
|
|
104
|
+
truncated: output.truncated,
|
|
105
|
+
notes: ["Fetched via Choose a License"],
|
|
106
|
+
};
|
|
107
|
+
} catch {}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
interface KevEntry {
|
|
5
|
+
cveID: string;
|
|
6
|
+
vendorProject?: string;
|
|
7
|
+
product?: string;
|
|
8
|
+
vulnerabilityName?: string;
|
|
9
|
+
shortDescription?: string;
|
|
10
|
+
requiredAction?: string;
|
|
11
|
+
dateAdded?: string;
|
|
12
|
+
dueDate?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface KevCatalog {
|
|
16
|
+
title?: string;
|
|
17
|
+
catalogVersion?: string;
|
|
18
|
+
dateReleased?: string;
|
|
19
|
+
count?: number;
|
|
20
|
+
vulnerabilities?: KevEntry[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CVE_PATTERN = /CVE-\d{4}-\d{4,7}/i;
|
|
24
|
+
const KEV_FEED_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Handle CISA Known Exploited Vulnerabilities (KEV) URLs
|
|
28
|
+
*/
|
|
29
|
+
export const handleCisaKev: SpecialHandler = async (
|
|
30
|
+
url: string,
|
|
31
|
+
timeout: number,
|
|
32
|
+
signal?: AbortSignal,
|
|
33
|
+
): Promise<RenderResult | null> => {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = new URL(url);
|
|
36
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
37
|
+
if (!hostname.endsWith("cisa.gov")) return null;
|
|
38
|
+
|
|
39
|
+
const path = parsed.pathname.toLowerCase();
|
|
40
|
+
if (!path.includes("known-exploited-vulnerabilities")) return null;
|
|
41
|
+
|
|
42
|
+
const cveMatch = parsed.pathname.match(CVE_PATTERN) ?? parsed.search.match(CVE_PATTERN);
|
|
43
|
+
if (!cveMatch) return null;
|
|
44
|
+
|
|
45
|
+
const cveId = cveMatch[0].toUpperCase();
|
|
46
|
+
const fetchedAt = new Date().toISOString();
|
|
47
|
+
|
|
48
|
+
const result = await loadPage(KEV_FEED_URL, {
|
|
49
|
+
timeout,
|
|
50
|
+
headers: { Accept: "application/json" },
|
|
51
|
+
signal,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (!result.ok) return null;
|
|
55
|
+
|
|
56
|
+
let data: KevCatalog;
|
|
57
|
+
try {
|
|
58
|
+
data = JSON.parse(result.content) as KevCatalog;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const entry = data.vulnerabilities?.find((item) => item.cveID?.toUpperCase() === cveId);
|
|
64
|
+
if (!entry) return null;
|
|
65
|
+
|
|
66
|
+
let md = `# ${entry.cveID}\n\n`;
|
|
67
|
+
if (entry.vulnerabilityName) {
|
|
68
|
+
md += `${entry.vulnerabilityName}\n\n`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
md += "## Metadata\n\n";
|
|
72
|
+
if (entry.vendorProject) md += `**Vendor:** ${entry.vendorProject}\n`;
|
|
73
|
+
if (entry.product) md += `**Product:** ${entry.product}\n`;
|
|
74
|
+
if (entry.dateAdded) md += `**Date Added:** ${entry.dateAdded}\n`;
|
|
75
|
+
if (entry.dueDate) md += `**Due Date:** ${entry.dueDate}\n`;
|
|
76
|
+
md += "\n";
|
|
77
|
+
|
|
78
|
+
if (entry.shortDescription) {
|
|
79
|
+
md += `## Description\n\n${entry.shortDescription}\n\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (entry.requiredAction) {
|
|
83
|
+
md += `## Required Action\n\n${entry.requiredAction}\n\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const output = finalizeOutput(md);
|
|
87
|
+
return {
|
|
88
|
+
url,
|
|
89
|
+
finalUrl: url,
|
|
90
|
+
contentType: "text/markdown",
|
|
91
|
+
method: "cisa-kev",
|
|
92
|
+
content: output.content,
|
|
93
|
+
fetchedAt,
|
|
94
|
+
truncated: output.truncated,
|
|
95
|
+
notes: ["Fetched via CISA KEV feed"],
|
|
96
|
+
};
|
|
97
|
+
} catch {}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
};
|