gityo 1.0.10 → 1.0.11
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 +18 -1
- package/dist/index.js +223 -14
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -154,10 +154,27 @@ Example:
|
|
|
154
154
|
"autoAcceptMessage": false,
|
|
155
155
|
"postCommand": "push",
|
|
156
156
|
"autoRunPostCommand": false,
|
|
157
|
-
"instructions": "Write short, clear commit messages."
|
|
157
|
+
"instructions": "Write short, clear commit messages.",
|
|
158
|
+
"maxDiffTokens": 24000,
|
|
159
|
+
"perFileCap": 400
|
|
158
160
|
}
|
|
159
161
|
```
|
|
160
162
|
|
|
163
|
+
### Large changes
|
|
164
|
+
|
|
165
|
+
When the diff is small, it is sent to the model as-is. When it is too large for one request, gityo minimizes it first:
|
|
166
|
+
|
|
167
|
+
- regenerates the diff with minimal context lines
|
|
168
|
+
- drops lock files and minified/generated files from the payload
|
|
169
|
+
- caps each file's patch and lists every changed file with its line counts so the model still sees the full picture
|
|
170
|
+
|
|
171
|
+
If the minimized diff is still too large, each remaining part is summarized in parallel and a final commit message is generated from those summaries.
|
|
172
|
+
|
|
173
|
+
Two optional config knobs control this:
|
|
174
|
+
|
|
175
|
+
- `maxDiffTokens` — estimated token budget for the diff sent to the model (default `24000`)
|
|
176
|
+
- `perFileCap` — max diff lines kept per file when minimizing (default `400`)
|
|
177
|
+
|
|
161
178
|
Example instructions file:
|
|
162
179
|
|
|
163
180
|
```md
|
package/dist/index.js
CHANGED
|
@@ -1710,7 +1710,7 @@ function useColor() {
|
|
|
1710
1710
|
if (process$1.env.FORCE_COLOR || process$1.env.CLICOLOR_FORCE !== void 0) return true;
|
|
1711
1711
|
}
|
|
1712
1712
|
new Command();
|
|
1713
|
-
var version$4 = "1.0.
|
|
1713
|
+
var version$4 = "1.0.11";
|
|
1714
1714
|
Object.freeze({ status: "aborted" });
|
|
1715
1715
|
function $constructor$3(name$16, initializer$6, params) {
|
|
1716
1716
|
function init$1(inst, def) {
|
|
@@ -175938,6 +175938,8 @@ const configSchema = object$4({
|
|
|
175938
175938
|
models: record$3(string$6().min(1), modelSchema),
|
|
175939
175939
|
autoAcceptMessage: boolean$5(),
|
|
175940
175940
|
instructions: string$6().min(1),
|
|
175941
|
+
maxDiffTokens: number$5().int().min(1e3),
|
|
175942
|
+
perFileCap: number$5().int().min(50),
|
|
175941
175943
|
postCommand: _enum$4(["push", "push-and-pull"]).nullable(),
|
|
175942
175944
|
autoRunPostCommand: boolean$5()
|
|
175943
175945
|
}).partial();
|
|
@@ -175947,6 +175949,8 @@ function resolveConfig(input) {
|
|
|
175947
175949
|
models: parsed.models,
|
|
175948
175950
|
instructions: parsed.instructions,
|
|
175949
175951
|
autoAcceptCommitMessage: parsed.autoAcceptMessage ?? false,
|
|
175952
|
+
maxDiffTokens: parsed.maxDiffTokens,
|
|
175953
|
+
perFileCap: parsed.perFileCap,
|
|
175950
175954
|
postCommand: parsed.postCommand === void 0 ? "push" : parsed.postCommand,
|
|
175951
175955
|
autoRunPostCommand: parsed.autoRunPostCommand ?? false
|
|
175952
175956
|
};
|
|
@@ -181074,17 +181078,23 @@ async function getChangedFiles(git) {
|
|
|
181074
181078
|
...splitNull(untracked)
|
|
181075
181079
|
])).sort((left, right) => left.localeCompare(right));
|
|
181076
181080
|
}
|
|
181077
|
-
async function getCommitDiff(git) {
|
|
181081
|
+
async function getCommitDiff(git, contextLines = 3) {
|
|
181082
|
+
const unified = [`-U${contextLines}`];
|
|
181078
181083
|
const staged = await git.raw([
|
|
181079
181084
|
"diff",
|
|
181080
181085
|
"--cached",
|
|
181081
|
-
"--no-ext-diff"
|
|
181086
|
+
"--no-ext-diff",
|
|
181087
|
+
...unified
|
|
181082
181088
|
]);
|
|
181083
181089
|
if (staged.trim().length > 0) return {
|
|
181084
181090
|
diff: staged,
|
|
181085
181091
|
hasStaged: true
|
|
181086
181092
|
};
|
|
181087
|
-
const [unstaged, untracked] = await Promise.all([git.raw([
|
|
181093
|
+
const [unstaged, untracked] = await Promise.all([git.raw([
|
|
181094
|
+
"diff",
|
|
181095
|
+
"--no-ext-diff",
|
|
181096
|
+
...unified
|
|
181097
|
+
]), git.raw([
|
|
181088
181098
|
"ls-files",
|
|
181089
181099
|
"--others",
|
|
181090
181100
|
"--exclude-standard",
|
|
@@ -181094,6 +181104,7 @@ async function getCommitDiff(git) {
|
|
|
181094
181104
|
diff: [unstaged, ...await Promise.all(splitNull(untracked).map((file$4) => git.raw([
|
|
181095
181105
|
"diff",
|
|
181096
181106
|
"--no-index",
|
|
181107
|
+
...unified,
|
|
181097
181108
|
"--",
|
|
181098
181109
|
"/dev/null",
|
|
181099
181110
|
file$4
|
|
@@ -181112,20 +181123,178 @@ function createLiveGit(baseDir) {
|
|
|
181112
181123
|
});
|
|
181113
181124
|
return git;
|
|
181114
181125
|
}
|
|
181126
|
+
const DEFAULT_MAX_DIFF_TOKENS = 24e3;
|
|
181127
|
+
const DEFAULT_PER_FILE_CAP = 400;
|
|
181128
|
+
const CHARS_PER_TOKEN = 4;
|
|
181129
|
+
const MINIMIZED_CONTEXT_LINES = 1;
|
|
181130
|
+
const FILE_SECTION_PREFIX = "diff --git ";
|
|
181131
|
+
const HUNK_HEADER_PREFIX = "@@ ";
|
|
181132
|
+
const NOISE_FILE_PATTERNS = [/\.(lock|min\.(?:js|mjs|cjs|css)|map)$/i, /-lock\.[^/]+$/i];
|
|
181133
|
+
function estimateTokens(text$1) {
|
|
181134
|
+
return Math.ceil(text$1.length / CHARS_PER_TOKEN);
|
|
181135
|
+
}
|
|
181136
|
+
async function minimizeDiff(git, options) {
|
|
181137
|
+
const { diff } = await getCommitDiff(git, MINIMIZED_CONTEXT_LINES);
|
|
181138
|
+
const sections = splitFileSections(diff).map(parseSection);
|
|
181139
|
+
const kept = [];
|
|
181140
|
+
const excludedPaths = /* @__PURE__ */ new Set();
|
|
181141
|
+
for (const section of sections) if (isNoiseFile(section.path)) excludedPaths.add(section.path);
|
|
181142
|
+
else kept.push(section);
|
|
181143
|
+
return {
|
|
181144
|
+
toc: buildToc(sections, excludedPaths, options.allFiles),
|
|
181145
|
+
body: kept.map((section) => capSection(section, options.perFileCap)).join("\n")
|
|
181146
|
+
};
|
|
181147
|
+
}
|
|
181148
|
+
function splitDiffIntoChunks(diff, maxTokens) {
|
|
181149
|
+
return packIntoChunks(splitFileSections(diff).flatMap((section) => estimateTokens(section) <= maxTokens ? [section] : splitOversizedSection(section, maxTokens)), maxTokens);
|
|
181150
|
+
}
|
|
181151
|
+
function isNoiseFile(path$4) {
|
|
181152
|
+
return NOISE_FILE_PATTERNS.some((pattern) => pattern.test(path$4));
|
|
181153
|
+
}
|
|
181154
|
+
function splitFileSections(diff) {
|
|
181155
|
+
return ("\n" + diff.trimStart()).split("\ndiff --git ").slice(1).map((part) => FILE_SECTION_PREFIX + part);
|
|
181156
|
+
}
|
|
181157
|
+
function parseSection(section) {
|
|
181158
|
+
let additions = 0;
|
|
181159
|
+
let deletions = 0;
|
|
181160
|
+
let binary = false;
|
|
181161
|
+
for (const line of section.split("\n").slice(1)) {
|
|
181162
|
+
if (line.startsWith("Binary files") || line.startsWith("GIT binary patch")) {
|
|
181163
|
+
binary = true;
|
|
181164
|
+
continue;
|
|
181165
|
+
}
|
|
181166
|
+
if (line.startsWith("+++") || line.startsWith("---")) continue;
|
|
181167
|
+
if (line.startsWith("+")) additions++;
|
|
181168
|
+
else if (line.startsWith("-")) deletions++;
|
|
181169
|
+
}
|
|
181170
|
+
return {
|
|
181171
|
+
path: extractPath(section.split("\n")[0]),
|
|
181172
|
+
content: section,
|
|
181173
|
+
additions,
|
|
181174
|
+
deletions,
|
|
181175
|
+
binary
|
|
181176
|
+
};
|
|
181177
|
+
}
|
|
181178
|
+
function extractPath(header) {
|
|
181179
|
+
const match = header.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
181180
|
+
return match ? match[2] : "";
|
|
181181
|
+
}
|
|
181182
|
+
function capSection(section, cap) {
|
|
181183
|
+
const lines = section.content.split("\n");
|
|
181184
|
+
const bodyLines = lines.length - 1;
|
|
181185
|
+
if (bodyLines <= cap) return section.content;
|
|
181186
|
+
return [...lines.slice(0, cap + 1), `[... ${bodyLines - cap} more lines truncated]`].join("\n");
|
|
181187
|
+
}
|
|
181188
|
+
function buildToc(sections, excludedPaths, allFiles) {
|
|
181189
|
+
const lines = ["Changed files:"];
|
|
181190
|
+
const seen = /* @__PURE__ */ new Set();
|
|
181191
|
+
for (const section of sections) {
|
|
181192
|
+
seen.add(section.path);
|
|
181193
|
+
if (section.binary) {
|
|
181194
|
+
lines.push(`- ${section.path} (binary)`);
|
|
181195
|
+
continue;
|
|
181196
|
+
}
|
|
181197
|
+
lines.push(`- ${section.path} (+${section.additions} -${section.deletions})`);
|
|
181198
|
+
}
|
|
181199
|
+
for (const path$4 of excludedPaths) {
|
|
181200
|
+
seen.add(path$4);
|
|
181201
|
+
lines.push(`- ${path$4} (excluded generated file)`);
|
|
181202
|
+
}
|
|
181203
|
+
for (const file$4 of allFiles) if (!seen.has(file$4)) lines.push(`- ${file$4}`);
|
|
181204
|
+
return lines.join("\n");
|
|
181205
|
+
}
|
|
181206
|
+
function packIntoChunks(pieces, maxTokens) {
|
|
181207
|
+
const chunks = [];
|
|
181208
|
+
let current = "";
|
|
181209
|
+
for (const piece of pieces) {
|
|
181210
|
+
if (current.length > 0 && estimateTokens(current + piece) > maxTokens) {
|
|
181211
|
+
chunks.push(current);
|
|
181212
|
+
current = "";
|
|
181213
|
+
}
|
|
181214
|
+
current += piece;
|
|
181215
|
+
}
|
|
181216
|
+
if (current.length > 0) chunks.push(current);
|
|
181217
|
+
return chunks;
|
|
181218
|
+
}
|
|
181219
|
+
function splitOversizedSection(section, maxTokens) {
|
|
181220
|
+
const [header, ...lines] = section.split("\n");
|
|
181221
|
+
const hunks = [];
|
|
181222
|
+
let currentHunk = [];
|
|
181223
|
+
for (const line of lines) {
|
|
181224
|
+
if (line.startsWith(HUNK_HEADER_PREFIX) && currentHunk.length > 0) {
|
|
181225
|
+
hunks.push(currentHunk);
|
|
181226
|
+
currentHunk = [];
|
|
181227
|
+
}
|
|
181228
|
+
currentHunk.push(line);
|
|
181229
|
+
}
|
|
181230
|
+
if (currentHunk.length > 0) hunks.push(currentHunk);
|
|
181231
|
+
return packIntoChunks(hunks.flatMap((hunk) => {
|
|
181232
|
+
const piece = [header, ...hunk].join("\n");
|
|
181233
|
+
return estimateTokens(piece) <= maxTokens ? [piece] : hardSplit(piece, maxTokens);
|
|
181234
|
+
}), maxTokens);
|
|
181235
|
+
}
|
|
181236
|
+
function hardSplit(text$1, maxTokens) {
|
|
181237
|
+
const maxChars = maxTokens * CHARS_PER_TOKEN;
|
|
181238
|
+
const chunks = [];
|
|
181239
|
+
let current = "";
|
|
181240
|
+
for (const line of text$1.split("\n")) {
|
|
181241
|
+
if (estimateTokens(line) > maxTokens) {
|
|
181242
|
+
if (current.length > 0) {
|
|
181243
|
+
chunks.push(current);
|
|
181244
|
+
current = "";
|
|
181245
|
+
}
|
|
181246
|
+
for (let index$2 = 0; index$2 < line.length; index$2 += maxChars) chunks.push(line.slice(index$2, index$2 + maxChars));
|
|
181247
|
+
continue;
|
|
181248
|
+
}
|
|
181249
|
+
const candidate = current.length === 0 ? line : `${current}\n${line}`;
|
|
181250
|
+
if (estimateTokens(candidate) > maxTokens) {
|
|
181251
|
+
chunks.push(current);
|
|
181252
|
+
current = line;
|
|
181253
|
+
continue;
|
|
181254
|
+
}
|
|
181255
|
+
current = candidate;
|
|
181256
|
+
}
|
|
181257
|
+
if (current.length > 0) chunks.push(current);
|
|
181258
|
+
return chunks;
|
|
181259
|
+
}
|
|
181115
181260
|
var system_prompt_default = "You are a strict Git commit message generator. ONLY output the commit message, nothing else.\n\nREQUIRED FORMAT:\n- First line: <type>(<scope>): <subject> (max 50 characters)\n- Types: feat, fix, docs, style, refactor, perf, test, chore, ci\n- Subject: imperative mood, lowercase, no period\n- If body needed: blank line + detailed explanation (wrapped at 72 chars)\n\nSTRICT RULES:\n1. MUST use conventional commits format: type(scope): subject\n2. Subject line MUST be under 50 characters\n3. Subject MUST be in imperative mood (e.g., \"add\", \"fix\", \"update\")\n4. Subject MUST be lowercase\n5. Subject MUST NOT end with a period\n6. MUST provide clear context about WHAT changed and WHY\n7. MUST be specific about affected functions, components, or modules\n8. MUST NOT use vague terms like \"Update\", \"Fix stuff\", \"Changes\"\n9. If including a body, MUST have exactly one blank line between subject and body\n10. Body lines MUST wrap at 72 characters\n\nOUTPUT INSTRUCTION:\nReturn ONLY the commit message. No explanations, no extra text, no markdown formatting.\n\nEXAMPLE:\nfeat(llm): add support for streaming responses\n\nImplement streaming response handling for commit message\ngeneration to improve user experience with large diffs.\nAdds new stream event handlers and updates the API contract.\n\nAnalyze the diff and generate a message following ALL rules above.";
|
|
181116
|
-
|
|
181261
|
+
const summarizePrompt = `You summarize parts of a large git diff for a commit message generator.
|
|
181262
|
+
Describe WHAT changed, in which files or modules, and any observable intent.
|
|
181263
|
+
Be factual and terse. Do NOT write a commit message. Maximum 120 words.`;
|
|
181264
|
+
async function ask(languageModel, system, messages) {
|
|
181117
181265
|
return (await generateText({
|
|
181118
181266
|
model: languageModel,
|
|
181119
|
-
instructions:
|
|
181120
|
-
messages
|
|
181121
|
-
role: "user",
|
|
181122
|
-
content: `Changes:\n${diff}`
|
|
181123
|
-
}, {
|
|
181124
|
-
role: "user",
|
|
181125
|
-
content: instructions || "Generate a concise git commit message based on the above instructions and diff."
|
|
181126
|
-
}]
|
|
181267
|
+
instructions: system,
|
|
181268
|
+
messages
|
|
181127
181269
|
})).text.trim();
|
|
181128
181270
|
}
|
|
181271
|
+
async function generateCommitMessage(languageModel, instructions, diff) {
|
|
181272
|
+
return ask(languageModel, system_prompt_default, [{
|
|
181273
|
+
role: "user",
|
|
181274
|
+
content: `Changes:\n${diff}`
|
|
181275
|
+
}, {
|
|
181276
|
+
role: "user",
|
|
181277
|
+
content: instructions || "Generate a concise git commit message based on the above instructions and diff."
|
|
181278
|
+
}]);
|
|
181279
|
+
}
|
|
181280
|
+
async function summarizeChanges(languageModel, toc, chunk$1) {
|
|
181281
|
+
return ask(languageModel, summarizePrompt, [{
|
|
181282
|
+
role: "user",
|
|
181283
|
+
content: `All changed files:\n${toc}\n\nChanges (part of a larger diff):\n${chunk$1}`
|
|
181284
|
+
}, {
|
|
181285
|
+
role: "user",
|
|
181286
|
+
content: "Summarize these changes."
|
|
181287
|
+
}]);
|
|
181288
|
+
}
|
|
181289
|
+
async function generateCommitMessageFromSummaries(languageModel, instructions, toc, summaries) {
|
|
181290
|
+
return ask(languageModel, system_prompt_default, [{
|
|
181291
|
+
role: "user",
|
|
181292
|
+
content: `All changed files:\n${toc}\n\nSummaries of all changes:\n${summaries.map((summary, index$2) => `Part ${index$2 + 1}:\n${summary}`).join("\n\n")}`
|
|
181293
|
+
}, {
|
|
181294
|
+
role: "user",
|
|
181295
|
+
content: instructions || "Generate a concise git commit message covering ALL parts above."
|
|
181296
|
+
}]);
|
|
181297
|
+
}
|
|
181129
181298
|
function resolveLanguageModel(modelConfig) {
|
|
181130
181299
|
const envVarNames = Array.isArray(modelConfig.apiKeyEnv) ? modelConfig.apiKeyEnv : [modelConfig.apiKeyEnv];
|
|
181131
181300
|
let apiKey = null;
|
|
@@ -197959,6 +198128,10 @@ async function runWithLoading(label, task) {
|
|
|
197959
198128
|
readline.clearLine(process.stdout, 0);
|
|
197960
198129
|
}
|
|
197961
198130
|
}
|
|
198131
|
+
const MAP_CONCURRENCY = 3;
|
|
198132
|
+
const PROMPT_RESERVE_TOKENS = 800;
|
|
198133
|
+
const MIN_CHUNK_BUDGET_TOKENS = 1e3;
|
|
198134
|
+
const MAX_TOC_LINES = 500;
|
|
197962
198135
|
async function mainController(options = {}) {
|
|
197963
198136
|
const { git, liveGit } = await getGit();
|
|
197964
198137
|
const config$4 = await loadConfig((await git.revparse(["--show-toplevel"])).trim());
|
|
@@ -197992,7 +198165,15 @@ async function mainController(options = {}) {
|
|
|
197992
198165
|
}
|
|
197993
198166
|
if (finalCommitMessage.length === 0) while (true) {
|
|
197994
198167
|
if (forceLLMGenerate) console.log(source_default.yellow("• Using LLM to generate message"));
|
|
197995
|
-
finalCommitMessage = (await runWithLoading("Generating commit message", () =>
|
|
198168
|
+
finalCommitMessage = (await runWithLoading("Generating commit message", () => generateMessage({
|
|
198169
|
+
git,
|
|
198170
|
+
languageModel,
|
|
198171
|
+
instructions: config$4.instructions ?? null,
|
|
198172
|
+
diff,
|
|
198173
|
+
files,
|
|
198174
|
+
maxDiffTokens: config$4.maxDiffTokens ?? DEFAULT_MAX_DIFF_TOKENS,
|
|
198175
|
+
perFileCap: config$4.perFileCap ?? DEFAULT_PER_FILE_CAP
|
|
198176
|
+
}))).trim();
|
|
197996
198177
|
if (finalCommitMessage.length === 0) throw new Error("The selected model returned an empty commit message.");
|
|
197997
198178
|
console.log(source_default.cyan.dim(finalCommitMessage));
|
|
197998
198179
|
console.log("");
|
|
@@ -198012,6 +198193,34 @@ async function mainController(options = {}) {
|
|
|
198012
198193
|
await liveGit.push();
|
|
198013
198194
|
if (config$4.postCommand === "push-and-pull") await liveGit.pull();
|
|
198014
198195
|
}
|
|
198196
|
+
async function generateMessage(options) {
|
|
198197
|
+
const { git, languageModel, instructions, diff } = options;
|
|
198198
|
+
if (estimateTokens(diff) <= options.maxDiffTokens) return generateCommitMessage(languageModel, instructions, diff);
|
|
198199
|
+
console.log(source_default.yellow("• Large diff detected, minimizing"));
|
|
198200
|
+
const { toc, body } = await minimizeDiff(git, {
|
|
198201
|
+
perFileCap: options.perFileCap,
|
|
198202
|
+
allFiles: options.files
|
|
198203
|
+
});
|
|
198204
|
+
if (estimateTokens(toc) + estimateTokens(body) <= options.maxDiffTokens) return generateCommitMessage(languageModel, instructions, `${toc}\n\n${body}`);
|
|
198205
|
+
console.log(source_default.yellow("• Diff still too large, summarizing in parts"));
|
|
198206
|
+
let effectiveToc = toc;
|
|
198207
|
+
let chunkBudget = options.maxDiffTokens - estimateTokens(effectiveToc) - PROMPT_RESERVE_TOKENS;
|
|
198208
|
+
if (chunkBudget < MIN_CHUNK_BUDGET_TOKENS) {
|
|
198209
|
+
effectiveToc = toc.split("\n").slice(0, MAX_TOC_LINES).join("\n");
|
|
198210
|
+
chunkBudget = Math.max(options.maxDiffTokens - estimateTokens(effectiveToc) - PROMPT_RESERVE_TOKENS, MIN_CHUNK_BUDGET_TOKENS);
|
|
198211
|
+
}
|
|
198212
|
+
const chunks = splitDiffIntoChunks(body, chunkBudget);
|
|
198213
|
+
const summaries = new Array(chunks.length);
|
|
198214
|
+
let nextChunk = 0;
|
|
198215
|
+
async function worker() {
|
|
198216
|
+
while (nextChunk < chunks.length) {
|
|
198217
|
+
const index$2 = nextChunk++;
|
|
198218
|
+
summaries[index$2] = await summarizeChanges(languageModel, effectiveToc, chunks[index$2]);
|
|
198219
|
+
}
|
|
198220
|
+
}
|
|
198221
|
+
await Promise.all(Array.from({ length: Math.min(MAP_CONCURRENCY, chunks.length) }, worker));
|
|
198222
|
+
return generateCommitMessageFromSummaries(languageModel, instructions, effectiveToc, summaries);
|
|
198223
|
+
}
|
|
198015
198224
|
var AppUserCanceledError = class extends Error {};
|
|
198016
198225
|
function handleError(fn$1) {
|
|
198017
198226
|
Promise.resolve().then(fn$1).catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gityo",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/NazmusSayad/gityo.git"
|
|
7
|
+
},
|
|
4
8
|
"sideEffects": false,
|
|
5
9
|
"type": "module",
|
|
6
10
|
"scripts": {
|
|
@@ -13,7 +17,7 @@
|
|
|
13
17
|
"json": "tsx ./src/json.ts"
|
|
14
18
|
},
|
|
15
19
|
"bin": {
|
|
16
|
-
"gityo": "
|
|
20
|
+
"gityo": "dist/index.js"
|
|
17
21
|
},
|
|
18
22
|
"devDependencies": {
|
|
19
23
|
"@ai-sdk/amazon-bedrock": "^5.0.40",
|