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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli.js +115 -32
  3. 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 ({ pattern, path, glob: glob2, case_insensitive, max_results }) => {
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 = case_insensitive === true || String(case_insensitive) === "true";
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, no pleasantries, no apologies.
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).trim().slice(0, 80) || "untitled";
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
- async function summarizeMessage(model, text) {
2217
- const fallback = text.trim().slice(0, 80) || "untitled";
2218
- const prompt = `Summarize this user request as a short title, 3-6 words, no punctuation. Reply with the title only.
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
- Request:
2221
- ${text.slice(0, 2e3)}`;
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
- return out.trim().split("\n").filter(Boolean)[0]?.trim() || fallback;
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 === "+" ? "#13351f" : ln.sign === "-" ? "#3b1414" : void 0,
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
- summarizeResult(result, toolName)
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("thinking\u2026");
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("thinking\u2026");
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("thinking\u2026");
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
- if (agent.agentHistory.length) persistSession(sessionId, agent.agentHistory);
3398
- }, [agent.agentHistory, sessionId]);
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.16",
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
- "agent",
43
+ "llm",
44
+ "local-llm",
45
+ "local-first",
46
+ "offline",
47
+ "privacy",
48
+ "terminal",
49
+ "tui",
36
50
  "ink",
37
- "tui"
51
+ "agent",
52
+ "pair-programming",
53
+ "code-generation",
54
+ "llama-cpp",
55
+ "lm-studio"
38
56
  ],
39
57
  "license": "MIT",
40
58
  "dependencies": {