miii-agent 0.1.16 → 0.1.18
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/LICENSE +21 -0
- package/dist/cli.js +115 -32
- package/package.json +22 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 maruakshay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/cli.js
CHANGED
|
@@ -851,26 +851,45 @@ var init_run_bash = __esm({
|
|
|
851
851
|
|
|
852
852
|
// src/tools/grep.ts
|
|
853
853
|
import { execa as execa2 } from "execa";
|
|
854
|
-
var grep;
|
|
854
|
+
var bool, grep;
|
|
855
855
|
var init_grep = __esm({
|
|
856
856
|
"src/tools/grep.ts"() {
|
|
857
857
|
"use strict";
|
|
858
858
|
init_paths();
|
|
859
|
+
bool = (v) => v === true || String(v) === "true";
|
|
859
860
|
grep = {
|
|
860
861
|
name: "grep",
|
|
861
862
|
description: "Search file contents for a regex pattern. Uses ripgrep if available, falls back to grep -R.",
|
|
862
863
|
input_schema: {
|
|
863
864
|
type: "object",
|
|
864
865
|
properties: {
|
|
865
|
-
pattern: { type: "string", description: "Regex pattern" },
|
|
866
|
+
pattern: { type: "string", description: "Regex pattern (literal when fixed_strings)" },
|
|
866
867
|
path: { type: "string", description: "Root path to search (default cwd)" },
|
|
867
868
|
glob: { type: "string", description: 'File glob filter, e.g. "*.ts"' },
|
|
868
869
|
case_insensitive: { type: "boolean", description: "Case-insensitive match" },
|
|
869
|
-
max_results: { type: "number", description: "Max matching lines (default 200)" }
|
|
870
|
+
max_results: { type: "number", description: "Max matching lines (default 200)" },
|
|
871
|
+
context: { type: "number", description: "Lines of context before & after each match" },
|
|
872
|
+
files_only: { type: "boolean", description: "List matching filenames only" },
|
|
873
|
+
type: { type: "string", description: 'ripgrep file type filter, e.g. "js" (ignored by grep fallback)' },
|
|
874
|
+
multiline: { type: "boolean", description: "Allow matches to span multiple lines" },
|
|
875
|
+
count: { type: "boolean", description: "Print count of matching lines per file" },
|
|
876
|
+
fixed_strings: { type: "boolean", description: "Treat pattern as literal string, not regex" }
|
|
870
877
|
},
|
|
871
878
|
required: ["pattern"]
|
|
872
879
|
},
|
|
873
|
-
handler: async ({
|
|
880
|
+
handler: async ({
|
|
881
|
+
pattern,
|
|
882
|
+
path,
|
|
883
|
+
glob: glob2,
|
|
884
|
+
case_insensitive,
|
|
885
|
+
max_results,
|
|
886
|
+
context,
|
|
887
|
+
files_only,
|
|
888
|
+
type,
|
|
889
|
+
multiline,
|
|
890
|
+
count,
|
|
891
|
+
fixed_strings
|
|
892
|
+
}) => {
|
|
874
893
|
let root;
|
|
875
894
|
try {
|
|
876
895
|
root = confinePath(path ?? ".");
|
|
@@ -878,21 +897,37 @@ var init_grep = __esm({
|
|
|
878
897
|
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
879
898
|
}
|
|
880
899
|
const limit = max_results ?? 200;
|
|
881
|
-
const ci =
|
|
900
|
+
const ci = bool(case_insensitive);
|
|
901
|
+
const filesOnly = bool(files_only);
|
|
902
|
+
const ml = bool(multiline);
|
|
903
|
+
const cnt = bool(count);
|
|
904
|
+
const fixed = bool(fixed_strings);
|
|
905
|
+
const ctx = typeof context === "number" && context > 0 ? Math.floor(context) : 0;
|
|
882
906
|
const tryRg = async () => {
|
|
883
907
|
const args2 = ["--line-number", "--no-heading", "--color=never", "-m", String(limit)];
|
|
884
908
|
if (ci) args2.push("-i");
|
|
909
|
+
if (fixed) args2.push("-F");
|
|
885
910
|
if (glob2) args2.push("--glob", glob2);
|
|
911
|
+
if (type) args2.push("-t", type);
|
|
912
|
+
if (ctx) args2.push("-C", String(ctx));
|
|
913
|
+
if (ml) args2.push("-U", "--multiline-dotall");
|
|
914
|
+
if (filesOnly) args2.push("-l");
|
|
915
|
+
if (cnt) args2.push("-c");
|
|
886
916
|
args2.push("--", pattern, root);
|
|
887
917
|
return execa2("rg", args2, { reject: false, timeout: 2e4 });
|
|
888
918
|
};
|
|
889
919
|
const tryGrep = async () => {
|
|
890
920
|
const args2 = ["-R", "-n", "--color=never"];
|
|
891
921
|
if (ci) args2.push("-i");
|
|
922
|
+
if (fixed) args2.push("-F");
|
|
892
923
|
if (glob2) args2.push("--include", glob2);
|
|
924
|
+
if (ctx) args2.push("-C", String(ctx));
|
|
925
|
+
if (filesOnly) args2.push("-l");
|
|
926
|
+
if (cnt) args2.push("-c");
|
|
893
927
|
args2.push("--", pattern, root);
|
|
894
928
|
return execa2("grep", args2, { reject: false, timeout: 2e4 });
|
|
895
929
|
};
|
|
930
|
+
const missing = (err) => err?.code === "ENOENT" || err?.errno === "ENOENT";
|
|
896
931
|
try {
|
|
897
932
|
let res;
|
|
898
933
|
try {
|
|
@@ -900,7 +935,8 @@ var init_grep = __esm({
|
|
|
900
935
|
if (res.exitCode === 127 || (res.stderr ?? "").includes("command not found")) {
|
|
901
936
|
res = await tryGrep();
|
|
902
937
|
}
|
|
903
|
-
} catch {
|
|
938
|
+
} catch (err) {
|
|
939
|
+
if (!missing(err)) throw err;
|
|
904
940
|
res = await tryGrep();
|
|
905
941
|
}
|
|
906
942
|
const lines = (res.stdout ?? "").split("\n").slice(0, limit);
|
|
@@ -1148,9 +1184,19 @@ This prevents drift. Each step attends to the original goal, not just the previo
|
|
|
1148
1184
|
|
|
1149
1185
|
# Output format
|
|
1150
1186
|
- Always reply in plain text. Never use Markdown syntax: no \`#\` headings, no \`**bold**\`, no \`-\` bullet lists, no fenced \`\`\` code blocks, no inline backticks.
|
|
1187
|
+
- This applies to your reasoning/thinking too. Write internal thoughts in plain text \u2014 no Markdown headings, bold, lists, or code fences there either.
|
|
1151
1188
|
- Quote code, paths, and identifiers inline as plain text. Do not wrap them.
|
|
1152
1189
|
- Keep prose terse.
|
|
1153
1190
|
|
|
1191
|
+
# Tone and voice
|
|
1192
|
+
- Sound like a calm, caring teammate who is genuinely invested in the user's goal. Warm, steady, reassuring \u2014 never cold or robotic.
|
|
1193
|
+
- Lead with empathy, especially when the user is stuck, frustrated, or facing a hard bug. Acknowledge the difficulty briefly before diving in: "That's a tricky one \u2014 let's work through it together."
|
|
1194
|
+
- Be encouraging about the goal. Treat it as something worth caring about, and convey quiet confidence that you'll reach it together.
|
|
1195
|
+
- Stay honest and direct. Empathy never means hedging, sugarcoating, or hiding bad news. Deliver hard truths kindly but plainly.
|
|
1196
|
+
- Keep warmth lightweight: a sentence or a few words, not gushing. One genuine, human touch beats a paragraph of pleasantries.
|
|
1197
|
+
- Mind the user's effort and context. If something will take a while or carry risk, say so gently and set expectations.
|
|
1198
|
+
- Celebrate progress in passing \u2014 a fixed bug, a passing test \u2014 without slowing the work down.
|
|
1199
|
+
|
|
1154
1200
|
# Engineering mindset
|
|
1155
1201
|
- Treat every request as one of: bug, feature, or fix. Name which one before you start.
|
|
1156
1202
|
- Apply first principles: decompose unclear tasks into smallest concrete sub-problems, solve each explicitly, compose the result.
|
|
@@ -1189,7 +1235,7 @@ ${toolLines}
|
|
|
1189
1235
|
- Prefer editing existing files over creating new ones.
|
|
1190
1236
|
- For edit_file, make old_str unique by including surrounding context, or set replace_all to change every occurrence.
|
|
1191
1237
|
- Never invent file paths. Read, glob, or grep before editing.
|
|
1192
|
-
- No filler,
|
|
1238
|
+
- No empty filler or robotic boilerplate. A brief, genuine warm touch (see Tone and voice) is welcome; hollow pleasantries and reflexive apologies are not.
|
|
1193
1239
|
|
|
1194
1240
|
# Context discipline
|
|
1195
1241
|
- read_file returns line numbers and accepts offset/limit. For large files, grep or glob to the relevant region first, then read only that range with offset/limit. Do not read a whole large file when you need a few functions \u2014 it wastes the context window.
|
|
@@ -2119,7 +2165,7 @@ function messageText(m) {
|
|
|
2119
2165
|
function firstUserText(messages) {
|
|
2120
2166
|
const first = messages.find((m) => m.role === "user");
|
|
2121
2167
|
if (!first) return "untitled";
|
|
2122
|
-
return messageText(first)
|
|
2168
|
+
return flattenForTitle(messageText(first)).slice(0, 80) || "untitled";
|
|
2123
2169
|
}
|
|
2124
2170
|
function readMeta(id) {
|
|
2125
2171
|
try {
|
|
@@ -2151,6 +2197,10 @@ function persistSession(id, messages, title) {
|
|
|
2151
2197
|
}
|
|
2152
2198
|
writeFileSync2(sessionPath(id), lines.join("\n") + "\n", "utf-8");
|
|
2153
2199
|
}
|
|
2200
|
+
function setSessionTitle(id, title) {
|
|
2201
|
+
if (!readMeta(id)) return;
|
|
2202
|
+
persistSession(id, loadSession(id), title);
|
|
2203
|
+
}
|
|
2154
2204
|
function listSessions() {
|
|
2155
2205
|
if (!existsSync2(SESSION_DIR)) return [];
|
|
2156
2206
|
const metas = [];
|
|
@@ -2213,12 +2263,35 @@ function toDisplayMessages(history) {
|
|
|
2213
2263
|
}
|
|
2214
2264
|
return out;
|
|
2215
2265
|
}
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2266
|
+
function flattenForTitle(text) {
|
|
2267
|
+
return text.replace(/<[^>]*>/g, " ").replace(/[`*_#>|]/g, " ").replace(/https?:\/\/\S+/g, " ").replace(/\s+/g, " ").trim();
|
|
2268
|
+
}
|
|
2269
|
+
function looksLikeJunkTitle(title) {
|
|
2270
|
+
return !title || /[<>]/.test(title) || title.length > 80;
|
|
2271
|
+
}
|
|
2272
|
+
async function summarizeConversation(model, messages) {
|
|
2273
|
+
const parts = [];
|
|
2274
|
+
let sawUser = false;
|
|
2275
|
+
let sawAssistant = false;
|
|
2276
|
+
for (const m of messages) {
|
|
2277
|
+
if (m.role === "system") continue;
|
|
2278
|
+
const t = flattenForTitle(messageText(m));
|
|
2279
|
+
if (!t) continue;
|
|
2280
|
+
if (m.role === "user" && !sawUser) {
|
|
2281
|
+
parts.push(`User: ${t}`);
|
|
2282
|
+
sawUser = true;
|
|
2283
|
+
} else if (m.role === "assistant" && !sawAssistant) {
|
|
2284
|
+
parts.push(`Assistant: ${t}`);
|
|
2285
|
+
sawAssistant = true;
|
|
2286
|
+
}
|
|
2287
|
+
if (sawUser && sawAssistant) break;
|
|
2288
|
+
}
|
|
2289
|
+
const convo = parts.join("\n").slice(0, 2e3);
|
|
2290
|
+
const fallback = (parts[0]?.replace(/^User: /, "") ?? "").slice(0, 80) || "untitled";
|
|
2291
|
+
const prompt = `Summarize this conversation as a short title, 3-6 words, no punctuation. Reply with the title only.
|
|
2219
2292
|
|
|
2220
|
-
|
|
2221
|
-
${
|
|
2293
|
+
Conversation:
|
|
2294
|
+
${convo}`;
|
|
2222
2295
|
try {
|
|
2223
2296
|
let out = "";
|
|
2224
2297
|
for await (const chunk of chat3(
|
|
@@ -2229,7 +2302,8 @@ ${text.slice(0, 2e3)}`;
|
|
|
2229
2302
|
)) {
|
|
2230
2303
|
if (chunk.content) out += chunk.content;
|
|
2231
2304
|
}
|
|
2232
|
-
|
|
2305
|
+
const title = out.trim().split("\n").filter(Boolean)[0]?.trim() ?? "";
|
|
2306
|
+
return looksLikeJunkTitle(title) ? fallback : title;
|
|
2233
2307
|
} catch {
|
|
2234
2308
|
return fallback;
|
|
2235
2309
|
}
|
|
@@ -2495,7 +2569,7 @@ function FileEditBlock({
|
|
|
2495
2569
|
Text9,
|
|
2496
2570
|
{
|
|
2497
2571
|
wrap: "truncate",
|
|
2498
|
-
backgroundColor: ln.sign === "
|
|
2572
|
+
backgroundColor: ln.sign === "-" ? "#3b1414" : ln.sign === "+" && label !== "Write" ? "#13351f" : void 0,
|
|
2499
2573
|
dimColor: ln.sign === " ",
|
|
2500
2574
|
children: [
|
|
2501
2575
|
`${ln.sign} `,
|
|
@@ -2587,10 +2661,11 @@ function ToolResultBlock({ result, toolName }) {
|
|
|
2587
2661
|
const visible = expanded ? lines : lines.slice(0, COLLAPSED_LINES);
|
|
2588
2662
|
const shown = visible.map((l) => truncate2(l, MAX_LINE_WIDTH));
|
|
2589
2663
|
const extra = lines.length - shown.length;
|
|
2664
|
+
const header = toolName === "grep" || toolName === "glob" ? summarizeResult(result, toolName) : `${lines.length} line${lines.length === 1 ? "" : "s"}`;
|
|
2590
2665
|
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginLeft: 2, children: [
|
|
2591
2666
|
/* @__PURE__ */ jsxs9(Text9, { color: result.is_error ? "red" : void 0, dimColor: !result.is_error, children: [
|
|
2592
2667
|
"\u23BF ",
|
|
2593
|
-
|
|
2668
|
+
header
|
|
2594
2669
|
] }),
|
|
2595
2670
|
shown.map((ln, i) => /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsx9(Text9, { color: result.is_error ? "red" : void 0, dimColor: true, children: ln || " " }) }, i)),
|
|
2596
2671
|
extra > 0 && /* @__PURE__ */ jsx9(Box9, { marginLeft: 4, children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
@@ -2781,7 +2856,7 @@ function useAgentRunner(model, activeCtx) {
|
|
|
2781
2856
|
if (busyRef.current || !model) return;
|
|
2782
2857
|
busyRef.current = true;
|
|
2783
2858
|
setBusy(true);
|
|
2784
|
-
setProcessingLabel("
|
|
2859
|
+
setProcessingLabel("crunching\u2026");
|
|
2785
2860
|
setError(null);
|
|
2786
2861
|
setMessages((prev) => [...prev, { role: "user", content: text }]);
|
|
2787
2862
|
setThinking(true);
|
|
@@ -2863,7 +2938,7 @@ function useAgentRunner(model, activeCtx) {
|
|
|
2863
2938
|
case "thinking-delta": {
|
|
2864
2939
|
thinkingAcc += ev.text;
|
|
2865
2940
|
setThinking(true);
|
|
2866
|
-
setProcessingLabel("
|
|
2941
|
+
setProcessingLabel("crunching\u2026");
|
|
2867
2942
|
flushThink();
|
|
2868
2943
|
break;
|
|
2869
2944
|
}
|
|
@@ -2880,7 +2955,7 @@ function useAgentRunner(model, activeCtx) {
|
|
|
2880
2955
|
is_error: ev.block.is_error
|
|
2881
2956
|
});
|
|
2882
2957
|
setActiveToolResults([...turnResults]);
|
|
2883
|
-
setProcessingLabel("
|
|
2958
|
+
setProcessingLabel("crunching\u2026");
|
|
2884
2959
|
break;
|
|
2885
2960
|
}
|
|
2886
2961
|
case "turn-end": {
|
|
@@ -3018,6 +3093,7 @@ function useKeyboard(opts) {
|
|
|
3018
3093
|
setFilePickerCursor,
|
|
3019
3094
|
sessionId,
|
|
3020
3095
|
setSessionId,
|
|
3096
|
+
onResumeSession,
|
|
3021
3097
|
sessions,
|
|
3022
3098
|
setSessions,
|
|
3023
3099
|
setNotice,
|
|
@@ -3190,6 +3266,7 @@ function useKeyboard(opts) {
|
|
|
3190
3266
|
setActiveToolResults([]);
|
|
3191
3267
|
setError(null);
|
|
3192
3268
|
setSessionId(meta.id);
|
|
3269
|
+
onResumeSession(meta.id);
|
|
3193
3270
|
setNotice(`resumed \xB7 ${meta.title}`);
|
|
3194
3271
|
setState("ready");
|
|
3195
3272
|
}
|
|
@@ -3288,17 +3365,6 @@ function useKeyboard(opts) {
|
|
|
3288
3365
|
} else if (trimmed) {
|
|
3289
3366
|
setNotice(null);
|
|
3290
3367
|
const message = expandPastes(trimmed);
|
|
3291
|
-
if (!agentHistory.length && cfg.model) {
|
|
3292
|
-
const id = sessionId;
|
|
3293
|
-
const model = cfg.model;
|
|
3294
|
-
void (async () => {
|
|
3295
|
-
try {
|
|
3296
|
-
const title = await summarizeMessage(model, message);
|
|
3297
|
-
persistSession(id, [{ role: "user", content: message }], title);
|
|
3298
|
-
} catch {
|
|
3299
|
-
}
|
|
3300
|
-
})();
|
|
3301
|
-
}
|
|
3302
3368
|
sendMessage(message);
|
|
3303
3369
|
}
|
|
3304
3370
|
clearPasteStore();
|
|
@@ -3393,9 +3459,25 @@ function App() {
|
|
|
3393
3459
|
if (v) setUpdateAvailable(v);
|
|
3394
3460
|
});
|
|
3395
3461
|
}, []);
|
|
3462
|
+
const titledSessions = useRef2(/* @__PURE__ */ new Set());
|
|
3396
3463
|
useEffect4(() => {
|
|
3397
|
-
|
|
3398
|
-
|
|
3464
|
+
const history = agent.agentHistory;
|
|
3465
|
+
if (!history.length) return;
|
|
3466
|
+
persistSession(sessionId, history);
|
|
3467
|
+
if (!titledSessions.current.has(sessionId) && cfg.model && history.some((m) => m.role === "assistant")) {
|
|
3468
|
+
titledSessions.current.add(sessionId);
|
|
3469
|
+
const id = sessionId;
|
|
3470
|
+
const model = cfg.model;
|
|
3471
|
+
const snapshot = history;
|
|
3472
|
+
void (async () => {
|
|
3473
|
+
try {
|
|
3474
|
+
const title = await summarizeConversation(model, snapshot);
|
|
3475
|
+
setSessionTitle(id, title);
|
|
3476
|
+
} catch {
|
|
3477
|
+
}
|
|
3478
|
+
})();
|
|
3479
|
+
}
|
|
3480
|
+
}, [agent.agentHistory, sessionId, cfg.model]);
|
|
3399
3481
|
const loadGen = useRef2(0);
|
|
3400
3482
|
const loadModels = (afterProvider = false) => {
|
|
3401
3483
|
const gen = ++loadGen.current;
|
|
@@ -3466,6 +3548,7 @@ function App() {
|
|
|
3466
3548
|
setFilePickerCursor,
|
|
3467
3549
|
sessionId,
|
|
3468
3550
|
setSessionId,
|
|
3551
|
+
onResumeSession: (id) => titledSessions.current.add(id),
|
|
3469
3552
|
sessions,
|
|
3470
3553
|
setSessions,
|
|
3471
3554
|
setNotice,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miii-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Terminal AI coding agent powered by Ollama",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
-
"README.md"
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|
|
14
15
|
"node": ">=18"
|
|
@@ -28,13 +29,30 @@
|
|
|
28
29
|
"type": "git",
|
|
29
30
|
"url": "git+https://github.com/maruakshay/miii-cli.git"
|
|
30
31
|
},
|
|
32
|
+
"homepage": "https://github.com/maruakshay/miii-cli#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/maruakshay/miii-cli/issues"
|
|
35
|
+
},
|
|
31
36
|
"keywords": [
|
|
32
37
|
"cli",
|
|
33
38
|
"ai",
|
|
39
|
+
"ai-agent",
|
|
40
|
+
"coding-agent",
|
|
41
|
+
"ai-coding-assistant",
|
|
34
42
|
"ollama",
|
|
35
|
-
"
|
|
43
|
+
"llm",
|
|
44
|
+
"local-llm",
|
|
45
|
+
"local-first",
|
|
46
|
+
"offline",
|
|
47
|
+
"privacy",
|
|
48
|
+
"terminal",
|
|
49
|
+
"tui",
|
|
36
50
|
"ink",
|
|
37
|
-
"
|
|
51
|
+
"agent",
|
|
52
|
+
"pair-programming",
|
|
53
|
+
"code-generation",
|
|
54
|
+
"llama-cpp",
|
|
55
|
+
"lm-studio"
|
|
38
56
|
],
|
|
39
57
|
"license": "MIT",
|
|
40
58
|
"dependencies": {
|