@oh-my-pi/pi-coding-agent 3.30.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 +71 -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/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/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 +150 -74
- 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 +94 -83
- package/src/core/tools/task/index.ts +129 -92
- 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 +15 -6
- package/src/core/tools/task/worker.ts +124 -89
- package/src/core/tools/web-fetch.ts +112 -377
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
- 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-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
- package/src/core/tools/web-scrapers/fdroid.ts +158 -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-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
- 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-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
- 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-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -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-fetch-handlers → web-scrapers}/spotify.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
- 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-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
- 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/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/web-fetch-handlers/index.ts +0 -69
- package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
2
|
+
import { finalizeOutput, htmlToBasicMarkdown, loadPage } from "./types";
|
|
3
|
+
|
|
4
|
+
type JsonRecord = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
function asRecord(value: unknown): JsonRecord | null {
|
|
7
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
8
|
+
return value as JsonRecord;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getString(record: JsonRecord | null, key: string): string | undefined {
|
|
12
|
+
if (!record) return undefined;
|
|
13
|
+
const value = record[key];
|
|
14
|
+
return typeof value === "string" ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getRecord(record: JsonRecord | null, key: string): JsonRecord | null {
|
|
18
|
+
if (!record) return null;
|
|
19
|
+
return asRecord(record[key]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getArray(record: JsonRecord | null, key: string): unknown[] | undefined {
|
|
23
|
+
if (!record) return undefined;
|
|
24
|
+
const value = record[key];
|
|
25
|
+
return Array.isArray(value) ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractShortname(pathname: string): string | null {
|
|
29
|
+
const trimmed = pathname.replace(/\/+$/g, "");
|
|
30
|
+
const segments = trimmed.split("/").filter(Boolean);
|
|
31
|
+
|
|
32
|
+
if (segments.length < 2 || segments[0] !== "TR") return null;
|
|
33
|
+
|
|
34
|
+
if (segments.length === 2) {
|
|
35
|
+
const shortname = segments[1];
|
|
36
|
+
if (/^\d{4}$/.test(shortname)) return null;
|
|
37
|
+
return decodeURIComponent(shortname);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (segments.length >= 3 && /^\d{4}$/.test(segments[1])) {
|
|
41
|
+
const version = segments[2];
|
|
42
|
+
const match = version.match(/^[A-Za-z]+-(.+)-\d{8}$/);
|
|
43
|
+
if (match?.[1]) return decodeURIComponent(match[1]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeStatus(status?: string): { code?: string; label?: string } {
|
|
50
|
+
if (!status) return {};
|
|
51
|
+
const lower = status.toLowerCase();
|
|
52
|
+
|
|
53
|
+
if (lower.includes("working draft")) return { code: "WD", label: status };
|
|
54
|
+
if (lower.includes("candidate recommendation")) return { code: "CR", label: status };
|
|
55
|
+
if (lower.includes("proposed recommendation")) return { code: "PR", label: status };
|
|
56
|
+
if (lower.includes("recommendation")) return { code: "REC", label: status };
|
|
57
|
+
|
|
58
|
+
return { label: status };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractEditors(editorsPayload: JsonRecord | null): string[] {
|
|
62
|
+
const links = getRecord(editorsPayload, "_links");
|
|
63
|
+
const editors = getArray(links, "editors") ?? [];
|
|
64
|
+
const names: string[] = [];
|
|
65
|
+
|
|
66
|
+
for (const entry of editors) {
|
|
67
|
+
const record = asRecord(entry);
|
|
68
|
+
const title = getString(record, "title");
|
|
69
|
+
if (title) names.push(title);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return names;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const handleW3c: SpecialHandler = async (
|
|
76
|
+
url: string,
|
|
77
|
+
timeout: number,
|
|
78
|
+
signal?: AbortSignal,
|
|
79
|
+
): Promise<RenderResult | null> => {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = new URL(url);
|
|
82
|
+
if (parsed.hostname !== "www.w3.org" && parsed.hostname !== "w3.org") return null;
|
|
83
|
+
|
|
84
|
+
const shortname = extractShortname(parsed.pathname);
|
|
85
|
+
if (!shortname) return null;
|
|
86
|
+
|
|
87
|
+
const fetchedAt = new Date().toISOString();
|
|
88
|
+
|
|
89
|
+
const specUrl = `https://api.w3.org/specifications/${encodeURIComponent(shortname)}`;
|
|
90
|
+
const latestUrl = `https://api.w3.org/specifications/${encodeURIComponent(shortname)}/versions/latest`;
|
|
91
|
+
|
|
92
|
+
const [specResult, latestResult] = await Promise.all([
|
|
93
|
+
loadPage(specUrl, { timeout, signal, headers: { Accept: "application/json" } }),
|
|
94
|
+
loadPage(latestUrl, { timeout, signal, headers: { Accept: "application/json" } }),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
if (!specResult.ok || !latestResult.ok) return null;
|
|
98
|
+
|
|
99
|
+
const specPayload = asRecord(JSON.parse(specResult.content));
|
|
100
|
+
const latestPayload = asRecord(JSON.parse(latestResult.content));
|
|
101
|
+
if (!specPayload || !latestPayload) return null;
|
|
102
|
+
|
|
103
|
+
const title = getString(specPayload, "title");
|
|
104
|
+
const shortnameValue = getString(specPayload, "shortname") ?? shortname;
|
|
105
|
+
const description = getString(specPayload, "description") ?? getString(specPayload, "abstract");
|
|
106
|
+
const abstract = description ? htmlToBasicMarkdown(description) : undefined;
|
|
107
|
+
|
|
108
|
+
const latestVersionUrl =
|
|
109
|
+
getString(latestPayload, "uri") ??
|
|
110
|
+
getString(latestPayload, "shortlink") ??
|
|
111
|
+
getString(specPayload, "shortlink");
|
|
112
|
+
|
|
113
|
+
const latestStatus = getString(latestPayload, "status");
|
|
114
|
+
const normalizedStatus = normalizeStatus(latestStatus);
|
|
115
|
+
|
|
116
|
+
const specLinks = getRecord(specPayload, "_links");
|
|
117
|
+
const historyUrl = getString(getRecord(specLinks, "version-history"), "href");
|
|
118
|
+
|
|
119
|
+
const latestLinks = getRecord(latestPayload, "_links");
|
|
120
|
+
const editorsUrl = getString(getRecord(latestLinks, "editors"), "href");
|
|
121
|
+
|
|
122
|
+
let editors: string[] = [];
|
|
123
|
+
if (editorsUrl) {
|
|
124
|
+
const editorsResult = await loadPage(editorsUrl, { timeout: Math.min(timeout, 10), signal });
|
|
125
|
+
if (editorsResult.ok) {
|
|
126
|
+
try {
|
|
127
|
+
const editorsPayload = asRecord(JSON.parse(editorsResult.content));
|
|
128
|
+
editors = editorsPayload ? extractEditors(editorsPayload) : [];
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let md = `# ${title ?? shortnameValue}\n\n`;
|
|
134
|
+
if (abstract) md += `## Abstract\n\n${abstract}\n\n`;
|
|
135
|
+
|
|
136
|
+
md += "## Metadata\n\n";
|
|
137
|
+
md += `**Shortname:** ${shortnameValue}\n`;
|
|
138
|
+
if (normalizedStatus.code) {
|
|
139
|
+
md += `**Status:** ${normalizedStatus.code}`;
|
|
140
|
+
if (normalizedStatus.label) md += ` (${normalizedStatus.label})`;
|
|
141
|
+
md += "\n";
|
|
142
|
+
} else if (normalizedStatus.label) {
|
|
143
|
+
md += `**Status:** ${normalizedStatus.label}\n`;
|
|
144
|
+
}
|
|
145
|
+
if (editors.length) md += `**Editors:** ${editors.join(", ")}\n`;
|
|
146
|
+
if (latestVersionUrl) md += `**Latest Version:** ${latestVersionUrl}\n`;
|
|
147
|
+
if (historyUrl) md += `**History:** ${historyUrl}\n`;
|
|
148
|
+
|
|
149
|
+
const output = finalizeOutput(md);
|
|
150
|
+
return {
|
|
151
|
+
url,
|
|
152
|
+
finalUrl: latestVersionUrl ?? url,
|
|
153
|
+
contentType: "text/markdown",
|
|
154
|
+
method: "w3c-api",
|
|
155
|
+
content: output.content,
|
|
156
|
+
fetchedAt,
|
|
157
|
+
truncated: output.truncated,
|
|
158
|
+
notes: ["Fetched via W3C API"],
|
|
159
|
+
};
|
|
160
|
+
} catch {}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
};
|
|
@@ -90,7 +90,11 @@ type WikidataValue =
|
|
|
90
90
|
/**
|
|
91
91
|
* Handle Wikidata URLs via EntityData API
|
|
92
92
|
*/
|
|
93
|
-
export const handleWikidata: SpecialHandler = async (
|
|
93
|
+
export const handleWikidata: SpecialHandler = async (
|
|
94
|
+
url: string,
|
|
95
|
+
timeout: number,
|
|
96
|
+
signal?: AbortSignal,
|
|
97
|
+
): Promise<RenderResult | null> => {
|
|
94
98
|
try {
|
|
95
99
|
const parsed = new URL(url);
|
|
96
100
|
if (!parsed.hostname.includes("wikidata.org")) return null;
|
|
@@ -104,7 +108,7 @@ export const handleWikidata: SpecialHandler = async (url: string, timeout: numbe
|
|
|
104
108
|
|
|
105
109
|
// Fetch entity data from API
|
|
106
110
|
const apiUrl = `https://www.wikidata.org/wiki/Special:EntityData/${qid}.json`;
|
|
107
|
-
const result = await loadPage(apiUrl, { timeout });
|
|
111
|
+
const result = await loadPage(apiUrl, { timeout, signal });
|
|
108
112
|
|
|
109
113
|
if (!result.ok) return null;
|
|
110
114
|
|
|
@@ -149,7 +153,7 @@ export const handleWikidata: SpecialHandler = async (url: string, timeout: numbe
|
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
// Fetch labels for referenced entities (limit to 50)
|
|
152
|
-
const entityLabels = await resolveEntityLabels(Array.from(entityIdsToResolve).slice(0, 50), timeout);
|
|
156
|
+
const entityLabels = await resolveEntityLabels(Array.from(entityIdsToResolve).slice(0, 50), timeout, signal);
|
|
153
157
|
|
|
154
158
|
// Group claims by property
|
|
155
159
|
const processedProperties: string[] = [];
|
|
@@ -256,7 +260,11 @@ function getLocalizedAliases(
|
|
|
256
260
|
/**
|
|
257
261
|
* Resolve entity IDs to their labels via wbgetentities API
|
|
258
262
|
*/
|
|
259
|
-
async function resolveEntityLabels(
|
|
263
|
+
async function resolveEntityLabels(
|
|
264
|
+
entityIds: string[],
|
|
265
|
+
timeout: number,
|
|
266
|
+
signal?: AbortSignal,
|
|
267
|
+
): Promise<Record<string, string>> {
|
|
260
268
|
if (entityIds.length === 0) return {};
|
|
261
269
|
|
|
262
270
|
const labels: Record<string, string> = {};
|
|
@@ -268,7 +276,7 @@ async function resolveEntityLabels(entityIds: string[], timeout: number): Promis
|
|
|
268
276
|
const apiUrl = `https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${batch.join("|")}&props=labels&languages=en&format=json`;
|
|
269
277
|
|
|
270
278
|
try {
|
|
271
|
-
const result = await loadPage(apiUrl, { timeout: Math.min(timeout, 10) });
|
|
279
|
+
const result = await loadPage(apiUrl, { timeout: Math.min(timeout, 10), signal });
|
|
272
280
|
if (result.ok) {
|
|
273
281
|
const data = JSON.parse(result.content) as {
|
|
274
282
|
entities: Record<string, { labels?: Record<string, { value: string }> }>;
|
|
@@ -5,7 +5,11 @@ import { finalizeOutput, loadPage } from "./types";
|
|
|
5
5
|
/**
|
|
6
6
|
* Handle Wikipedia URLs via Wikipedia API
|
|
7
7
|
*/
|
|
8
|
-
export const handleWikipedia: SpecialHandler = async (
|
|
8
|
+
export const handleWikipedia: SpecialHandler = async (
|
|
9
|
+
url: string,
|
|
10
|
+
timeout: number,
|
|
11
|
+
signal?: AbortSignal,
|
|
12
|
+
): Promise<RenderResult | null> => {
|
|
9
13
|
try {
|
|
10
14
|
const parsed = new URL(url);
|
|
11
15
|
// Match *.wikipedia.org
|
|
@@ -21,7 +25,7 @@ export const handleWikipedia: SpecialHandler = async (url: string, timeout: numb
|
|
|
21
25
|
|
|
22
26
|
// Use Wikipedia API to get plain text extract
|
|
23
27
|
const apiUrl = `https://${lang}.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(title)}`;
|
|
24
|
-
const summaryResult = await loadPage(apiUrl, { timeout });
|
|
28
|
+
const summaryResult = await loadPage(apiUrl, { timeout, signal });
|
|
25
29
|
|
|
26
30
|
let md = "";
|
|
27
31
|
|
|
@@ -38,7 +42,7 @@ export const handleWikipedia: SpecialHandler = async (url: string, timeout: numb
|
|
|
38
42
|
|
|
39
43
|
// Get full article content via mobile-html or parse API
|
|
40
44
|
const contentUrl = `https://${lang}.wikipedia.org/api/rest_v1/page/mobile-html/${encodeURIComponent(title)}`;
|
|
41
|
-
const contentResult = await loadPage(contentUrl, { timeout });
|
|
45
|
+
const contentResult = await loadPage(contentUrl, { timeout, signal });
|
|
42
46
|
|
|
43
47
|
if (contentResult.ok) {
|
|
44
48
|
const doc = parseHtml(contentResult.content);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { unlinkSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import type { FileSink } from "bun";
|
|
5
|
+
import { nanoid } from "nanoid";
|
|
4
6
|
import { ensureTool } from "../../../utils/tools-manager";
|
|
5
7
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
6
8
|
import { finalizeOutput } from "./types";
|
|
@@ -8,20 +10,44 @@ import { finalizeOutput } from "./types";
|
|
|
8
10
|
/**
|
|
9
11
|
* Execute a command and return stdout
|
|
10
12
|
*/
|
|
11
|
-
function exec(
|
|
13
|
+
async function exec(
|
|
12
14
|
cmd: string,
|
|
13
15
|
args: string[],
|
|
14
|
-
options?: { timeout?: number; input?: string | Buffer },
|
|
15
|
-
): { stdout: string; stderr: string; ok: boolean } {
|
|
16
|
-
const
|
|
17
|
-
stdin: options?.input ?
|
|
16
|
+
options?: { timeout?: number; input?: string | Buffer; signal?: AbortSignal },
|
|
17
|
+
): Promise<{ stdout: string; stderr: string; ok: boolean; exitCode: number | null }> {
|
|
18
|
+
const proc = Bun.spawn([cmd, ...args], {
|
|
19
|
+
stdin: options?.input ? "pipe" : "ignore",
|
|
18
20
|
stdout: "pipe",
|
|
19
21
|
stderr: "pipe",
|
|
22
|
+
timeout: options?.timeout,
|
|
23
|
+
signal: options?.signal,
|
|
20
24
|
});
|
|
25
|
+
|
|
26
|
+
if (options?.input && proc.stdin) {
|
|
27
|
+
const stdin = proc.stdin as FileSink;
|
|
28
|
+
const payload = typeof options.input === "string" ? new TextEncoder().encode(options.input) : options.input;
|
|
29
|
+
stdin.write(payload);
|
|
30
|
+
const flushed = stdin.flush();
|
|
31
|
+
if (flushed instanceof Promise) {
|
|
32
|
+
await flushed;
|
|
33
|
+
}
|
|
34
|
+
const ended = stdin.end();
|
|
35
|
+
if (ended instanceof Promise) {
|
|
36
|
+
await ended;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const [stdout, stderr] = await Promise.all([
|
|
41
|
+
(proc.stdout as ReadableStream<Uint8Array>).text(),
|
|
42
|
+
(proc.stderr as ReadableStream<Uint8Array>).text(),
|
|
43
|
+
]);
|
|
44
|
+
const exitCode = await proc.exited;
|
|
45
|
+
|
|
21
46
|
return {
|
|
22
|
-
stdout
|
|
23
|
-
stderr
|
|
24
|
-
ok:
|
|
47
|
+
stdout,
|
|
48
|
+
stderr,
|
|
49
|
+
ok: exitCode === 0,
|
|
50
|
+
exitCode,
|
|
25
51
|
};
|
|
26
52
|
}
|
|
27
53
|
|
|
@@ -124,12 +150,18 @@ function formatDuration(seconds: number): string {
|
|
|
124
150
|
/**
|
|
125
151
|
* Handle YouTube URLs - fetch metadata and transcript
|
|
126
152
|
*/
|
|
127
|
-
export const handleYouTube: SpecialHandler = async (
|
|
153
|
+
export const handleYouTube: SpecialHandler = async (
|
|
154
|
+
url: string,
|
|
155
|
+
timeout: number,
|
|
156
|
+
signal?: AbortSignal,
|
|
157
|
+
): Promise<RenderResult | null> => {
|
|
158
|
+
signal?.throwIfAborted();
|
|
128
159
|
const yt = parseYouTubeUrl(url);
|
|
129
160
|
if (!yt) return null;
|
|
130
161
|
|
|
131
162
|
// Ensure yt-dlp is available (auto-download if missing)
|
|
132
163
|
const ytdlp = await ensureTool("yt-dlp", true);
|
|
164
|
+
signal?.throwIfAborted();
|
|
133
165
|
if (!ytdlp) {
|
|
134
166
|
return {
|
|
135
167
|
url,
|
|
@@ -148,9 +180,16 @@ export const handleYouTube: SpecialHandler = async (url: string, timeout: number
|
|
|
148
180
|
const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
|
|
149
181
|
|
|
150
182
|
// Fetch video metadata
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
183
|
+
signal?.throwIfAborted();
|
|
184
|
+
const metaResult = await exec(
|
|
185
|
+
ytdlp,
|
|
186
|
+
["--dump-json", "--no-warnings", "--no-playlist", "--skip-download", videoUrl],
|
|
187
|
+
{
|
|
188
|
+
timeout: timeout * 1000,
|
|
189
|
+
signal,
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
signal?.throwIfAborted();
|
|
154
193
|
|
|
155
194
|
let title = "YouTube Video";
|
|
156
195
|
let channel = "";
|
|
@@ -190,21 +229,29 @@ export const handleYouTube: SpecialHandler = async (url: string, timeout: number
|
|
|
190
229
|
let transcriptSource = "";
|
|
191
230
|
|
|
192
231
|
// First, list available subtitles
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
232
|
+
signal?.throwIfAborted();
|
|
233
|
+
const listResult = await exec(
|
|
234
|
+
ytdlp,
|
|
235
|
+
["--list-subs", "--no-warnings", "--no-playlist", "--skip-download", videoUrl],
|
|
236
|
+
{
|
|
237
|
+
timeout: timeout * 1000,
|
|
238
|
+
signal,
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
signal?.throwIfAborted();
|
|
196
242
|
|
|
197
243
|
const hasManualSubs = listResult.stdout.includes("[info] Available subtitles");
|
|
198
244
|
const hasAutoSubs = listResult.stdout.includes("[info] Available automatic captions");
|
|
199
245
|
|
|
200
246
|
// Create temp directory for subtitle download
|
|
201
247
|
const tmpDir = tmpdir();
|
|
202
|
-
const tmpBase = path.join(tmpDir, `yt-${yt.videoId}-${
|
|
248
|
+
const tmpBase = path.join(tmpDir, `yt-${yt.videoId}-${nanoid()}`);
|
|
203
249
|
|
|
204
250
|
try {
|
|
205
251
|
// Try manual subtitles first (English preferred)
|
|
206
252
|
if (hasManualSubs) {
|
|
207
|
-
|
|
253
|
+
signal?.throwIfAborted();
|
|
254
|
+
const subResult = await exec(
|
|
208
255
|
ytdlp,
|
|
209
256
|
[
|
|
210
257
|
"--write-sub",
|
|
@@ -219,13 +266,15 @@ export const handleYouTube: SpecialHandler = async (url: string, timeout: number
|
|
|
219
266
|
tmpBase,
|
|
220
267
|
videoUrl,
|
|
221
268
|
],
|
|
222
|
-
{ timeout: timeout * 1000 },
|
|
269
|
+
{ timeout: timeout * 1000, signal },
|
|
223
270
|
);
|
|
224
271
|
|
|
225
272
|
if (subResult.ok) {
|
|
226
273
|
// Find the downloaded subtitle file using glob
|
|
274
|
+
signal?.throwIfAborted();
|
|
227
275
|
const subFiles = await Array.fromAsync(new Bun.Glob(`${tmpBase}*.vtt`).scan({ absolute: true }));
|
|
228
276
|
if (subFiles.length > 0) {
|
|
277
|
+
signal?.throwIfAborted();
|
|
229
278
|
const vttContent = await Bun.file(subFiles[0]).text();
|
|
230
279
|
transcript = cleanVttToText(vttContent);
|
|
231
280
|
transcriptSource = "manual";
|
|
@@ -236,7 +285,8 @@ export const handleYouTube: SpecialHandler = async (url: string, timeout: number
|
|
|
236
285
|
|
|
237
286
|
// Fall back to auto-generated captions
|
|
238
287
|
if (!transcript && hasAutoSubs) {
|
|
239
|
-
|
|
288
|
+
signal?.throwIfAborted();
|
|
289
|
+
const autoResult = await exec(
|
|
240
290
|
ytdlp,
|
|
241
291
|
[
|
|
242
292
|
"--write-auto-sub",
|
|
@@ -251,12 +301,14 @@ export const handleYouTube: SpecialHandler = async (url: string, timeout: number
|
|
|
251
301
|
tmpBase,
|
|
252
302
|
videoUrl,
|
|
253
303
|
],
|
|
254
|
-
{ timeout: timeout * 1000 },
|
|
304
|
+
{ timeout: timeout * 1000, signal },
|
|
255
305
|
);
|
|
256
306
|
|
|
257
307
|
if (autoResult.ok) {
|
|
308
|
+
signal?.throwIfAborted();
|
|
258
309
|
const subFiles = await Array.fromAsync(new Bun.Glob(`${tmpBase}*.vtt`).scan({ absolute: true }));
|
|
259
310
|
if (subFiles.length > 0) {
|
|
311
|
+
signal?.throwIfAborted();
|
|
260
312
|
const vttContent = await Bun.file(subFiles[0]).text();
|
|
261
313
|
transcript = cleanVttToText(vttContent);
|
|
262
314
|
transcriptSource = "auto-generated";
|
package/src/core/tools/write.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/inte
|
|
|
6
6
|
import writeDescription from "../../prompts/tools/write.md" with { type: "text" };
|
|
7
7
|
import type { RenderResultOptions } from "../custom-tools/types";
|
|
8
8
|
import type { ToolSession } from "../sdk";
|
|
9
|
+
import { untilAborted } from "../utils";
|
|
9
10
|
import { createLspWritethrough, type FileDiagnosticsResult } from "./lsp/index";
|
|
10
11
|
import { resolveToCwd } from "./path-utils";
|
|
11
12
|
import { formatDiagnostics, replaceTabs, shortenPath } from "./render-utils";
|
|
@@ -34,27 +35,29 @@ export function createWriteTool(session: ToolSession): AgentTool<typeof writeSch
|
|
|
34
35
|
{ path, content }: { path: string; content: string },
|
|
35
36
|
signal?: AbortSignal,
|
|
36
37
|
) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
return untilAborted(signal, async () => {
|
|
39
|
+
const absolutePath = resolveToCwd(path, session.cwd);
|
|
40
|
+
|
|
41
|
+
const diagnostics = await writethrough(absolutePath, content, signal);
|
|
42
|
+
|
|
43
|
+
let resultText = `Successfully wrote ${content.length} bytes to ${path}`;
|
|
44
|
+
if (!diagnostics) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text", text: resultText }],
|
|
47
|
+
details: {},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const messages = diagnostics?.messages;
|
|
52
|
+
if (messages && messages.length > 0) {
|
|
53
|
+
resultText += `\n\nLSP Diagnostics (${diagnostics.summary}):\n`;
|
|
54
|
+
resultText += messages.map((d) => ` ${d}`).join("\n");
|
|
55
|
+
}
|
|
43
56
|
return {
|
|
44
57
|
content: [{ type: "text", text: resultText }],
|
|
45
|
-
details: {},
|
|
58
|
+
details: { diagnostics },
|
|
46
59
|
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const messages = diagnostics?.messages;
|
|
50
|
-
if (messages && messages.length > 0) {
|
|
51
|
-
resultText += `\n\nLSP Diagnostics (${diagnostics.summary}):\n`;
|
|
52
|
-
resultText += messages.map((d) => ` ${d}`).join("\n");
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
content: [{ type: "text", text: resultText }],
|
|
56
|
-
details: { diagnostics },
|
|
57
|
-
};
|
|
60
|
+
});
|
|
58
61
|
},
|
|
59
62
|
};
|
|
60
63
|
}
|
package/src/core/voice.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { unlinkSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { completeSimple, type Model } from "@mariozechner/pi-ai";
|
|
5
|
+
import { nanoid } from "nanoid";
|
|
5
6
|
import voiceSummaryPrompt from "../prompts/voice-summary.md" with { type: "text" };
|
|
6
7
|
import { logger } from "./logger";
|
|
7
8
|
import type { ModelRegistry } from "./model-registry";
|
|
@@ -99,7 +100,7 @@ function buildRecordingCommand(filePath: string, sampleRate: number, channels: n
|
|
|
99
100
|
export async function startVoiceRecording(_settings: VoiceSettings): Promise<VoiceRecordingHandle> {
|
|
100
101
|
const sampleRate = DEFAULT_SAMPLE_RATE;
|
|
101
102
|
const channels = DEFAULT_CHANNELS;
|
|
102
|
-
const filePath = join(tmpdir(), `omp-voice-${
|
|
103
|
+
const filePath = join(tmpdir(), `omp-voice-${nanoid()}.wav`);
|
|
103
104
|
const command = buildRecordingCommand(filePath, sampleRate, channels);
|
|
104
105
|
if (!command) {
|
|
105
106
|
throw new Error("No audio recorder found (install sox, arecord, or ffmpeg).");
|
|
@@ -233,7 +234,7 @@ function getPlayerCommand(filePath: string, format: VoiceSynthesisResult["format
|
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
export async function playAudio(audio: Uint8Array, format: VoiceSynthesisResult["format"]): Promise<void> {
|
|
236
|
-
const filePath = join(tmpdir(), `omp-
|
|
237
|
+
const filePath = join(tmpdir(), `omp-tts-${nanoid()}.${format}`);
|
|
237
238
|
await Bun.write(filePath, audio);
|
|
238
239
|
|
|
239
240
|
const command = getPlayerCommand(filePath, format);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
1
2
|
import { WorktreeError, WorktreeErrorCode } from "./errors";
|
|
2
3
|
import { git, gitWithStdin } from "./git";
|
|
3
4
|
import { find, remove, type Worktree } from "./operations";
|
|
@@ -89,7 +90,7 @@ async function collapseRebase(src: Worktree, dst: Worktree): Promise<string> {
|
|
|
89
90
|
throw new WorktreeError("Failed to resolve HEAD", WorktreeErrorCode.COLLAPSE_FAILED);
|
|
90
91
|
}
|
|
91
92
|
const originalHead = headResult.stdout.trim();
|
|
92
|
-
const tempBranch = `wt-collapse-${
|
|
93
|
+
const tempBranch = `wt-collapse-${nanoid()}`;
|
|
93
94
|
|
|
94
95
|
await requireGitSuccess(await git(["checkout", "-b", tempBranch], src.path), "Failed to create temp branch");
|
|
95
96
|
|
package/src/lib/worktree/git.ts
CHANGED
|
@@ -17,22 +17,6 @@ type WritableLike = {
|
|
|
17
17
|
|
|
18
18
|
const textEncoder = new TextEncoder();
|
|
19
19
|
|
|
20
|
-
async function readStream(stream: ReadableStream<Uint8Array> | undefined): Promise<string> {
|
|
21
|
-
if (!stream) return "";
|
|
22
|
-
const reader = stream.getReader();
|
|
23
|
-
const chunks: Uint8Array[] = [];
|
|
24
|
-
try {
|
|
25
|
-
while (true) {
|
|
26
|
-
const { done, value } = await reader.read();
|
|
27
|
-
if (done) break;
|
|
28
|
-
chunks.push(value);
|
|
29
|
-
}
|
|
30
|
-
} finally {
|
|
31
|
-
reader.releaseLock();
|
|
32
|
-
}
|
|
33
|
-
return Buffer.concat(chunks).toString();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
20
|
async function writeStdin(handle: unknown, stdin: string): Promise<void> {
|
|
37
21
|
if (!handle || typeof handle === "number") return;
|
|
38
22
|
if (typeof (handle as WritableStream<Uint8Array>).getWriter === "function") {
|
|
@@ -77,8 +61,8 @@ export async function gitWithStdin(args: string[], stdin: string, cwd?: string):
|
|
|
77
61
|
await writeStdin(proc.stdin, stdin);
|
|
78
62
|
|
|
79
63
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
80
|
-
|
|
81
|
-
|
|
64
|
+
(proc.stdout as ReadableStream<Uint8Array>).text(),
|
|
65
|
+
(proc.stderr as ReadableStream<Uint8Array>).text(),
|
|
82
66
|
proc.exited,
|
|
83
67
|
]);
|
|
84
68
|
|
package/src/main.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { type ImageContent, supportsXhigh } from "@mariozechner/pi-ai";
|
|
9
9
|
import chalk from "chalk";
|
|
10
|
+
import { homedir, tmpdir } from "node:os";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
10
12
|
import { type Args, parseArgs, printHelp } from "./cli/args";
|
|
11
13
|
import { processFileArguments } from "./cli/file-processor";
|
|
12
14
|
import { listModels } from "./cli/list-models";
|
|
@@ -187,6 +189,59 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
|
|
|
187
189
|
return undefined;
|
|
188
190
|
}
|
|
189
191
|
|
|
192
|
+
async function maybeAutoChdir(parsed: Args): Promise<void> {
|
|
193
|
+
if (parsed.allowHome || parsed.cwd) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const home = homedir();
|
|
198
|
+
if (!home) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const normalizePath = (value: string) => {
|
|
203
|
+
const resolved = resolve(value);
|
|
204
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const cwd = normalizePath(process.cwd());
|
|
208
|
+
const normalizedHome = normalizePath(home);
|
|
209
|
+
if (cwd !== normalizedHome) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const isDirectory = async (path: string) => {
|
|
214
|
+
try {
|
|
215
|
+
const stat = await Bun.file(path).stat();
|
|
216
|
+
return stat.isDirectory();
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const candidates = [join(home, "tmp"), "/tmp", "/var/tmp"];
|
|
223
|
+
for (const candidate of candidates) {
|
|
224
|
+
try {
|
|
225
|
+
if (!(await isDirectory(candidate))) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
process.chdir(candidate);
|
|
229
|
+
return;
|
|
230
|
+
} catch {
|
|
231
|
+
// Try next candidate.
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const fallback = tmpdir();
|
|
237
|
+
if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) {
|
|
238
|
+
process.chdir(fallback);
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
// Ignore fallback errors.
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
190
245
|
/** Discover SYSTEM.md file if no CLI system prompt was provided */
|
|
191
246
|
function discoverSystemPromptFile(): string | undefined {
|
|
192
247
|
// Check project-local first (.omp/SYSTEM.md, .pi/SYSTEM.md legacy)
|
|
@@ -318,6 +373,10 @@ export async function main(args: string[]) {
|
|
|
318
373
|
return;
|
|
319
374
|
}
|
|
320
375
|
|
|
376
|
+
const parsed = parseArgs(args);
|
|
377
|
+
time("parseArgs");
|
|
378
|
+
await maybeAutoChdir(parsed);
|
|
379
|
+
|
|
321
380
|
// Run migrations (pass cwd for project-local migrations)
|
|
322
381
|
const { migratedAuthProviders: migratedProviders, deprecationWarnings } = await runMigrations(process.cwd());
|
|
323
382
|
|
|
@@ -326,9 +385,6 @@ export async function main(args: string[]) {
|
|
|
326
385
|
const modelRegistry = await discoverModels(authStorage);
|
|
327
386
|
time("discoverModels");
|
|
328
387
|
|
|
329
|
-
const parsed = parseArgs(args);
|
|
330
|
-
time("parseArgs");
|
|
331
|
-
|
|
332
388
|
if (parsed.version) {
|
|
333
389
|
console.log(VERSION);
|
|
334
390
|
return;
|