miii-agent 0.1.26 → 0.1.27

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/README.md +16 -1
  2. package/dist/cli.js +192 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,10 +30,12 @@ Your code never leaves your disk. There's nothing to log in to. Pull a model, ty
30
30
 
31
31
  ```bash
32
32
  ollama pull qwen2.5-coder:14b # any coding model works
33
- npm install -g miii-agent
33
+ curl -fsSL https://raw.githubusercontent.com/maruakshay/miii-cli/main/install.sh | sh
34
34
  miii
35
35
  ```
36
36
 
37
+ Prefer npm? `npm install -g miii-agent`.
38
+
37
39
  Then just talk to it:
38
40
 
39
41
  ```
@@ -44,6 +46,19 @@ Then just talk to it:
44
46
 
45
47
  > **Needs:** Node ≥ 18 and [Ollama](https://ollama.com/download) running locally.
46
48
 
49
+ ## Staying up to date
50
+
51
+ miii checks npm on launch and, when a newer release exists, pulls it in the
52
+ background — it applies the next time you start. Manual options:
53
+
54
+ ```bash
55
+ miii update # update now
56
+ miii --version # what you're running
57
+ ```
58
+
59
+ Opt out of background updates by adding `"autoUpdate": false` to `~/.miii/config.json`,
60
+ or re-run the install script (`curl … | sh`) any time to update by hand.
61
+
47
62
  ## Why local-first?
48
63
 
49
64
  Most "AI coding tools" are just wrappers around a cloud API — slow, metered, and they ship your private codebase to someone else's server.
package/dist/cli.js CHANGED
@@ -35,9 +35,13 @@ function migrate(raw) {
35
35
  provider: raw.provider,
36
36
  effort: raw.effort,
37
37
  providers,
38
- modelContexts: raw.modelContexts
38
+ modelContexts: raw.modelContexts,
39
+ autoUpdate: raw.autoUpdate
39
40
  };
40
41
  }
42
+ function autoUpdateEnabled(cfg = loadConfig()) {
43
+ return cfg.autoUpdate !== false;
44
+ }
41
45
  function readRawConfig() {
42
46
  if (!existsSync(CONFIG_PATH)) return {};
43
47
  try {
@@ -1303,7 +1307,8 @@ Ask in a numbered list. One round of questions per turn. Then wait.
1303
1307
 
1304
1308
  # Tool calls
1305
1309
  - When you need a tool, emit the tool call directly. No preamble, no narration, no "I will use X".
1306
- - Never describe a tool call instead of emitting it. If you cannot emit the call, answer in plain text.
1310
+ - Use the native function-calling interface as the ONLY channel for tool calls. Never print a tool call as text \u2014 not as JSON, not as a fenced code block, not as \`call:name{...}\`, not in any custom or tagged syntax. Text-form calls do NOT execute; they leak to the user and nothing happens.
1311
+ - If you cannot emit a real function call, do not fake one in prose \u2014 answer in plain text instead.
1307
1312
  - After a tool result, move directly to the next tool call or the final answer. Do not restate what the previous tool did.
1308
1313
 
1309
1314
  # Tools
@@ -1475,6 +1480,11 @@ function parseTextToolCalls(text, knownToolNames) {
1475
1480
  if (!text) return { calls: [], cleanedText: text };
1476
1481
  const calls = [];
1477
1482
  let cleaned = text;
1483
+ const callSyntax = parseCallSyntax(cleaned, knownToolNames);
1484
+ if (callSyntax.calls.length > 0) {
1485
+ calls.push(...callSyntax.calls);
1486
+ cleaned = callSyntax.cleanedText;
1487
+ }
1478
1488
  const tagRe = /<\|?tool_call\|?>\s*([\s\S]*?)\s*<\|?\/?tool_call\|?>/g;
1479
1489
  cleaned = cleaned.replace(tagRe, (_m, body) => {
1480
1490
  const c = tryParse(body, knownToolNames);
@@ -1515,6 +1525,74 @@ function tryParse(raw, knownToolNames) {
1515
1525
  return null;
1516
1526
  }
1517
1527
  }
1528
+ function parseCallSyntax(text, knownToolNames) {
1529
+ const calls = [];
1530
+ const headRe = /call:\s*([a-zA-Z_]\w*)\s*\{/g;
1531
+ let m;
1532
+ let cleaned = "";
1533
+ let lastEnd = 0;
1534
+ while ((m = headRe.exec(text)) !== null) {
1535
+ const name = m[1];
1536
+ const body = parseCallBody(text, m.index + m[0].length);
1537
+ if (!body) continue;
1538
+ headRe.lastIndex = body.end;
1539
+ if (!knownToolNames.includes(name)) continue;
1540
+ calls.push({ function: { name, arguments: body.args } });
1541
+ cleaned += text.slice(lastEnd, m.index);
1542
+ lastEnd = body.end;
1543
+ }
1544
+ cleaned += text.slice(lastEnd);
1545
+ return { calls, cleanedText: cleaned.trim() };
1546
+ }
1547
+ function parseCallBody(text, start) {
1548
+ const args2 = {};
1549
+ let i = start;
1550
+ const isWs = (c) => c === " " || c === " " || c === "\n" || c === "\r";
1551
+ while (i < text.length) {
1552
+ while (i < text.length && (isWs(text[i]) || text[i] === ",")) i++;
1553
+ if (i >= text.length) return null;
1554
+ if (text[i] === "}") return { args: args2, end: i + 1 };
1555
+ const keyStart = i;
1556
+ while (i < text.length && text[i] !== ":" && text[i] !== "}") i++;
1557
+ if (i >= text.length || text[i] !== ":") return null;
1558
+ const key = text.slice(keyStart, i).trim().replace(/^["']|["']$/g, "");
1559
+ i++;
1560
+ while (i < text.length && isWs(text[i])) i++;
1561
+ if (text.startsWith(VAL_DELIM, i)) {
1562
+ const vStart = i + VAL_DELIM.length;
1563
+ const vEnd = text.indexOf(VAL_DELIM, vStart);
1564
+ if (vEnd === -1) return null;
1565
+ args2[key] = text.slice(vStart, vEnd);
1566
+ i = vEnd + VAL_DELIM.length;
1567
+ } else {
1568
+ const vStart = i;
1569
+ let depth = 0;
1570
+ let inStr = null;
1571
+ let esc = false;
1572
+ while (i < text.length) {
1573
+ const ch = text[i];
1574
+ if (inStr) {
1575
+ if (esc) esc = false;
1576
+ else if (ch === "\\") esc = true;
1577
+ else if (ch === inStr) inStr = null;
1578
+ } else if (ch === '"' || ch === "'") inStr = ch;
1579
+ else if (ch === "{" || ch === "[") depth++;
1580
+ else if (ch === "}" || ch === "]") {
1581
+ if (depth === 0) break;
1582
+ depth--;
1583
+ } else if (ch === "," && depth === 0) break;
1584
+ i++;
1585
+ }
1586
+ const rawVal = text.slice(vStart, i).trim();
1587
+ try {
1588
+ args2[key] = JSON.parse(rawVal);
1589
+ } catch {
1590
+ args2[key] = rawVal.replace(/^["']|["']$/g, "");
1591
+ }
1592
+ }
1593
+ }
1594
+ return null;
1595
+ }
1518
1596
  function extractFirstJsonObject(s) {
1519
1597
  const start = s.indexOf("{");
1520
1598
  if (start === -1) return null;
@@ -1541,6 +1619,18 @@ function extractFirstJsonObject(s) {
1541
1619
  }
1542
1620
  return null;
1543
1621
  }
1622
+ function looksLikeLeakedToolCall(text, knownToolNames) {
1623
+ if (!text) return false;
1624
+ if (text.includes(VAL_DELIM)) return true;
1625
+ if (/<\|?tool_call/i.test(text)) return true;
1626
+ const callMatch = text.match(/\bcall:\s*(\w+)\s*\{/);
1627
+ if (callMatch && knownToolNames.includes(callMatch[1])) return true;
1628
+ if (/"name"\s*:\s*"([^"]+)"/.test(text) && /"(arguments|parameters|input)"\s*:/.test(text)) {
1629
+ const name = text.match(/"name"\s*:\s*"([^"]+)"/)?.[1];
1630
+ if (name && knownToolNames.includes(name)) return true;
1631
+ }
1632
+ return false;
1633
+ }
1544
1634
  function blocksFromOllama(text, tool_calls, knownToolNames = []) {
1545
1635
  const blocks = [];
1546
1636
  let finalText = text;
@@ -1563,9 +1653,11 @@ function blocksFromOllama(text, tool_calls, knownToolNames = []) {
1563
1653
  }
1564
1654
  return blocks;
1565
1655
  }
1656
+ var VAL_DELIM;
1566
1657
  var init_adapter = __esm({
1567
1658
  "src/agent/adapter.ts"() {
1568
1659
  "use strict";
1660
+ VAL_DELIM = '<|"|>';
1569
1661
  }
1570
1662
  });
1571
1663
 
@@ -1638,6 +1730,7 @@ async function* runAgent(opts) {
1638
1730
  let evalTokens = 0;
1639
1731
  let lastAssistantSig = "";
1640
1732
  let repeatCount = 0;
1733
+ let leakNudges = 0;
1641
1734
  const seenPaths = /* @__PURE__ */ new Set();
1642
1735
  for (let turn = 0; turn < MAX_TURNS; turn++) {
1643
1736
  let text = "";
@@ -1722,6 +1815,16 @@ async function* runAgent(opts) {
1722
1815
  continue;
1723
1816
  }
1724
1817
  if (tool_uses.length === 0) {
1818
+ const assistantText = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
1819
+ if (leakNudges < MAX_LEAK_NUDGES && looksLikeLeakedToolCall(assistantText, toolNames)) {
1820
+ leakNudges++;
1821
+ history.push({
1822
+ role: "user",
1823
+ content: "That tool call was written as plain text, so it did not run and nothing happened. Re-issue it using the function-calling interface only \u2014 do not print the call as text, JSON, or any custom syntax. If you did not mean to call a tool, answer in prose."
1824
+ });
1825
+ yield { type: "turn-end", stop_reason: "tool_use" };
1826
+ continue;
1827
+ }
1725
1828
  yield { type: "turn-end", stop_reason: "end_turn" };
1726
1829
  break;
1727
1830
  }
@@ -1829,7 +1932,7 @@ async function* runAgent(opts) {
1829
1932
  yield { type: "done", prompt_tokens: promptTokens, eval_tokens: evalTokens };
1830
1933
  return history;
1831
1934
  }
1832
- var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL, BIG_WRITE_TOOLS;
1935
+ var MAX_TURNS, REPEAT_TAIL, REPEAT_KILL, MAX_LEAK_NUDGES, BIG_WRITE_TOOLS;
1833
1936
  var init_loop = __esm({
1834
1937
  "src/agent/loop.ts"() {
1835
1938
  "use strict";
@@ -1845,21 +1948,22 @@ var init_loop = __esm({
1845
1948
  MAX_TURNS = 25;
1846
1949
  REPEAT_TAIL = 120;
1847
1950
  REPEAT_KILL = 4;
1951
+ MAX_LEAK_NUDGES = 2;
1848
1952
  BIG_WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file"]);
1849
1953
  }
1850
1954
  });
1851
1955
 
1852
1956
  // eval/runner.ts
1853
- import { mkdtempSync, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, rmSync as rmSync4 } from "fs";
1854
- import { dirname as dirname3, join as join10 } from "path";
1957
+ import { mkdtempSync, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7, rmSync as rmSync4 } from "fs";
1958
+ import { dirname as dirname3, join as join11 } from "path";
1855
1959
  import { tmpdir as tmpdir2 } from "os";
1856
1960
  async function runScenario(model, s) {
1857
- const dir = mkdtempSync(join10(tmpdir2(), "miii-eval-"));
1961
+ const dir = mkdtempSync(join11(tmpdir2(), "miii-eval-"));
1858
1962
  const prevCwd = process.cwd();
1859
1963
  for (const [rel, content] of Object.entries(s.files ?? {})) {
1860
- const abs = join10(dir, rel);
1861
- mkdirSync6(dirname3(abs), { recursive: true });
1862
- writeFileSync7(abs, content, "utf-8");
1964
+ const abs = join11(dir, rel);
1965
+ mkdirSync7(dirname3(abs), { recursive: true });
1966
+ writeFileSync8(abs, content, "utf-8");
1863
1967
  }
1864
1968
  const r = {
1865
1969
  name: s.name,
@@ -1920,13 +2024,13 @@ var init_runner = __esm({
1920
2024
  });
1921
2025
 
1922
2026
  // eval/scenarios.ts
1923
- import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
1924
- import { join as join11 } from "path";
2027
+ import { readFileSync as readFileSync10, existsSync as existsSync8 } from "fs";
2028
+ import { join as join12 } from "path";
1925
2029
  var read, scenarios;
1926
2030
  var init_scenarios = __esm({
1927
2031
  "eval/scenarios.ts"() {
1928
2032
  "use strict";
1929
- read = (dir, f) => existsSync8(join11(dir, f)) ? readFileSync9(join11(dir, f), "utf-8") : null;
2033
+ read = (dir, f) => existsSync8(join12(dir, f)) ? readFileSync10(join12(dir, f), "utf-8") : null;
1930
2034
  scenarios = [
1931
2035
  {
1932
2036
  name: "edit-exact-string",
@@ -2076,13 +2180,25 @@ init_client();
2076
2180
  init_config();
2077
2181
  import { useState as useState5, useEffect as useEffect4, useRef as useRef2 } from "react";
2078
2182
  import { Box as Box13, Text as Text13, useApp } from "ink";
2079
- import { homedir as homedir7 } from "os";
2183
+ import { homedir as homedir8 } from "os";
2080
2184
  import { sep as sep2 } from "path";
2081
2185
 
2082
2186
  // src/ui/WelcomeBlock.tsx
2083
2187
  import { Box, Text } from "ink";
2084
2188
  import { jsx, jsxs } from "react/jsx-runtime";
2085
- function WelcomeBlock({ model, activeCtx, effort, cwd, updateAvailable }) {
2189
+ function updateBannerText(version, status) {
2190
+ switch (status) {
2191
+ case "downloading":
2192
+ return `\u2191 v${version} downloading in the background \u2014 restart miii to apply`;
2193
+ case "installed":
2194
+ return `\u2713 v${version} installed \u2014 restart miii to apply`;
2195
+ case "failed":
2196
+ return `\u2191 v${version} update failed \u2014 run \`miii update\` manually`;
2197
+ default:
2198
+ return `\u2191 v${version} available \u2014 run \`miii update\` to upgrade`;
2199
+ }
2200
+ }
2201
+ function WelcomeBlock({ model, activeCtx, effort, cwd, updateAvailable, updateStatus = "idle" }) {
2086
2202
  const ctxLabel = activeCtx != null ? `${Math.round(activeCtx / 1024)}k ctx` : "\u2014 ctx";
2087
2203
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
2088
2204
  /* @__PURE__ */ jsxs(
@@ -2109,7 +2225,7 @@ function WelcomeBlock({ model, activeCtx, effort, cwd, updateAvailable }) {
2109
2225
  ]
2110
2226
  }
2111
2227
  ),
2112
- updateAvailable && /* @__PURE__ */ jsx(Text, { color: "yellow", children: `\u2191 update available: v${updateAvailable} \u2014 run: miii --update` })
2228
+ updateAvailable && /* @__PURE__ */ jsx(Text, { color: updateStatus === "failed" ? "red" : updateStatus === "installed" ? "green" : "yellow", children: updateBannerText(updateAvailable, updateStatus) })
2113
2229
  ] });
2114
2230
  }
2115
2231
 
@@ -4477,8 +4593,28 @@ function useKeyboard(opts) {
4477
4593
 
4478
4594
  // src/updateCheck.ts
4479
4595
  import { createRequire } from "module";
4596
+ import { homedir as homedir7 } from "os";
4597
+ import { join as join10 } from "path";
4598
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6 } from "fs";
4480
4599
  var require2 = createRequire(import.meta.url);
4481
4600
  var PKG_NAME = "miii-agent";
4601
+ var UPDATE_ATTEMPT_PATH = join10(homedir7(), ".miii", ".last-update-attempt");
4602
+ var UPDATE_ATTEMPT_COOLDOWN_MS = 10 * 60 * 1e3;
4603
+ function recentlyAttemptedUpdate() {
4604
+ try {
4605
+ const last = Number(readFileSync9(UPDATE_ATTEMPT_PATH, "utf8").trim());
4606
+ return Number.isFinite(last) && Date.now() - last < UPDATE_ATTEMPT_COOLDOWN_MS;
4607
+ } catch {
4608
+ return false;
4609
+ }
4610
+ }
4611
+ function markUpdateAttempt() {
4612
+ try {
4613
+ mkdirSync6(join10(homedir7(), ".miii"), { recursive: true });
4614
+ writeFileSync7(UPDATE_ATTEMPT_PATH, String(Date.now()));
4615
+ } catch {
4616
+ }
4617
+ }
4482
4618
  function currentVersion() {
4483
4619
  try {
4484
4620
  return require2("../package.json").version;
@@ -4494,6 +4630,30 @@ function newerVersion(current, latest) {
4494
4630
  if (lb !== cb) return lb > cb;
4495
4631
  return lc > cc;
4496
4632
  }
4633
+ function autoUpdate(onDone) {
4634
+ if (recentlyAttemptedUpdate()) return false;
4635
+ try {
4636
+ markUpdateAttempt();
4637
+ const { spawn } = require2("child_process");
4638
+ const child = spawn("npm", ["i", "-g", `${PKG_NAME}@latest`], {
4639
+ detached: true,
4640
+ stdio: "ignore",
4641
+ shell: process.platform === "win32"
4642
+ });
4643
+ let settled = false;
4644
+ const settle = (ok) => {
4645
+ if (settled) return;
4646
+ settled = true;
4647
+ onDone?.(ok);
4648
+ };
4649
+ child.on("error", () => settle(false));
4650
+ child.on("exit", (code) => settle(code === 0));
4651
+ child.unref();
4652
+ return true;
4653
+ } catch {
4654
+ return false;
4655
+ }
4656
+ }
4497
4657
  async function checkForUpdate() {
4498
4658
  try {
4499
4659
  const res = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`, {
@@ -4514,7 +4674,7 @@ async function checkForUpdate() {
4514
4674
  import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
4515
4675
  function App() {
4516
4676
  const { exit } = useApp();
4517
- const cwd = process.cwd().replace(homedir7(), "~").split(sep2).join("/");
4677
+ const cwd = process.cwd().replace(homedir8(), "~").split(sep2).join("/");
4518
4678
  const [cfg, setCfg] = useState5(loadConfig());
4519
4679
  const [models, setModels] = useState5([]);
4520
4680
  const [contexts, setContexts] = useState5(() => cfg.modelContexts ?? {});
@@ -4525,6 +4685,7 @@ function App() {
4525
4685
  const [cursor, setCursor] = useState5(0);
4526
4686
  const [pickerQuery, setPickerQuery] = useState5("");
4527
4687
  const [updateAvailable, setUpdateAvailable] = useState5(null);
4688
+ const [updateStatus, setUpdateStatus] = useState5("idle");
4528
4689
  const [providerDown, setProviderDown] = useState5(false);
4529
4690
  const [sessionId, setSessionId] = useState5(() => newSessionId());
4530
4691
  const [sessions, setSessions] = useState5([]);
@@ -4536,7 +4697,12 @@ function App() {
4536
4697
  const agent = useAgentRunner(cfg.model, activeCtx);
4537
4698
  useEffect4(() => {
4538
4699
  checkForUpdate().then((v) => {
4539
- if (v) setUpdateAvailable(v);
4700
+ if (!v) return;
4701
+ setUpdateAvailable(v);
4702
+ if (autoUpdateEnabled()) {
4703
+ const started = autoUpdate((ok) => setUpdateStatus(ok ? "installed" : "failed"));
4704
+ if (started) setUpdateStatus("downloading");
4705
+ }
4540
4706
  });
4541
4707
  }, []);
4542
4708
  const titledSessions = useRef2(/* @__PURE__ */ new Set());
@@ -4653,7 +4819,7 @@ function App() {
4653
4819
  return Math.round(used / activeCtx * 100);
4654
4820
  })();
4655
4821
  return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, children: [
4656
- state !== "ready" && /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable }),
4822
+ state !== "ready" && /* @__PURE__ */ jsx13(WelcomeBlock, { model: cfg.model, activeCtx, effort, cwd, error: agent.error, updateAvailable, updateStatus }),
4657
4823
  state === "loading" && !agent.error && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: `connecting to ${provName}\u2026` }) }),
4658
4824
  agent.error && state !== "ready" && /* @__PURE__ */ jsx13(
4659
4825
  ChatView,
@@ -4714,7 +4880,8 @@ function App() {
4714
4880
  return /* @__PURE__ */ jsx13(FilePicker, { matches: searchFiles(process.cwd(), m.query), cursor: filePickerCursor });
4715
4881
  })(),
4716
4882
  /* @__PURE__ */ jsx13(InputBar, { input, caret, disabled: agent.busy, processingLabel: agent.processingLabel }),
4717
- !agent.busy && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: providerDown ? "provider unavailable \u2014 /provider to switch \xB7 /models to pick a model" : "type / to see commands" }) })
4883
+ !agent.busy && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { dimColor: true, children: providerDown ? "provider unavailable \u2014 /provider to switch \xB7 /models to pick a model" : "type / to see commands" }) }),
4884
+ updateAvailable && /* @__PURE__ */ jsx13(Box13, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx13(Text13, { color: updateStatus === "failed" ? "red" : updateStatus === "installed" ? "green" : "yellow", children: updateBannerText(updateAvailable, updateStatus) }) })
4718
4885
  ] })
4719
4886
  ] });
4720
4887
  }
@@ -4733,7 +4900,12 @@ for (let i = 0; i < args.length; i++) {
4733
4900
  cmd = args[i];
4734
4901
  }
4735
4902
  }
4736
- if (cmd === "update" || cmd === "--update" || cmd === "-u") {
4903
+ if (cmd === "version" || cmd === "--version" || cmd === "-v") {
4904
+ const { createRequire: createRequire2 } = await import("module");
4905
+ const pkg = createRequire2(import.meta.url)("../package.json");
4906
+ console.log(pkg.version);
4907
+ process.exit(0);
4908
+ } else if (cmd === "update" || cmd === "--update" || cmd === "-u") {
4737
4909
  const { spawnSync } = await import("child_process");
4738
4910
  console.log("Updating miii-agent\u2026");
4739
4911
  const r = spawnSync("npm", ["i", "-g", "miii-agent@latest"], { stdio: "inherit", shell: process.platform === "win32" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-agent",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "Cursor / Claude Code, but local. An offline AI pair-programmer in your terminal, powered by Ollama. Private by default, free forever.",
5
5
  "type": "module",
6
6
  "bin": {