interference-agent 0.1.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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/assets/screenshot.png +0 -0
  4. package/bun.lock +159 -0
  5. package/package.json +39 -0
  6. package/src/agent/compaction.ts +114 -0
  7. package/src/agent/loop.ts +94 -0
  8. package/src/agent/prompt.ts +89 -0
  9. package/src/agent/subagent.ts +64 -0
  10. package/src/auth.ts +50 -0
  11. package/src/cli-plain.ts +274 -0
  12. package/src/cli.ts +87 -0
  13. package/src/commands/index.ts +184 -0
  14. package/src/config-file.ts +109 -0
  15. package/src/config.ts +212 -0
  16. package/src/context.ts +96 -0
  17. package/src/cost.ts +54 -0
  18. package/src/git.ts +22 -0
  19. package/src/permissions.ts +135 -0
  20. package/src/provider.ts +58 -0
  21. package/src/session/__tests__/session.test.ts +180 -0
  22. package/src/session/snapshot.ts +122 -0
  23. package/src/session/store.ts +120 -0
  24. package/src/skills.ts +177 -0
  25. package/src/tools/__tests__/mutating.test.ts +324 -0
  26. package/src/tools/__tests__/question.test.ts +53 -0
  27. package/src/tools/__tests__/todowrite.test.ts +57 -0
  28. package/src/tools/__tests__/tools.test.ts +217 -0
  29. package/src/tools/_fs.ts +12 -0
  30. package/src/tools/bash.ts +104 -0
  31. package/src/tools/edit.ts +98 -0
  32. package/src/tools/glob.ts +40 -0
  33. package/src/tools/grep.ts +187 -0
  34. package/src/tools/index.ts +21 -0
  35. package/src/tools/ls.ts +70 -0
  36. package/src/tools/question.ts +81 -0
  37. package/src/tools/read.ts +61 -0
  38. package/src/tools/registry.ts +36 -0
  39. package/src/tools/task.ts +71 -0
  40. package/src/tools/todowrite.ts +84 -0
  41. package/src/tools/webfetch.ts +111 -0
  42. package/src/tools/write.ts +51 -0
  43. package/src/tui/App.tsx +738 -0
  44. package/src/tui/ConfirmDialog.tsx +46 -0
  45. package/src/tui/DiffView.tsx +88 -0
  46. package/src/tui/MarkdownText.tsx +63 -0
  47. package/src/tui/Message.tsx +26 -0
  48. package/src/tui/ModelPicker.tsx +44 -0
  49. package/src/tui/Panel.tsx +39 -0
  50. package/src/tui/ProviderPicker.tsx +111 -0
  51. package/src/tui/QuestionDialog.tsx +64 -0
  52. package/src/tui/SessionList.tsx +72 -0
  53. package/src/tui/SlashAutocomplete.tsx +33 -0
  54. package/src/tui/StatusFooter.tsx +71 -0
  55. package/src/tui/ThinkingPicker.tsx +57 -0
  56. package/src/tui/Toast.tsx +64 -0
  57. package/src/tui/TodoList.tsx +49 -0
  58. package/src/tui/ToolStep.tsx +184 -0
  59. package/src/tui/Welcome.tsx +87 -0
  60. package/src/tui/__tests__/tui-render.test.tsx +59 -0
  61. package/src/tui/theme.ts +16 -0
  62. package/src/tui/wordmark.ts +7 -0
  63. package/tsconfig.json +23 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present Interference contributors
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/README.md ADDED
@@ -0,0 +1,74 @@
1
+ <h1 align="center">interference</h1>
2
+
3
+ <p align="center"><strong>The open-source coding agent that lives in your terminal.</strong></p>
4
+
5
+ <p align="center">
6
+ TypeScript + Bun · Plan / Build modes · 9 tools · permissions · sessions with undo · 38 skills · subagents · TUI
7
+ </p>
8
+
9
+ ---
10
+
11
+ **interference** is an AI coding agent for the terminal. You describe a task; it explores your
12
+ codebase and edits files or runs commands through an agentic tool-calling loop — with explicit
13
+ permissions and a read-only **Plan** mode so nothing happens without your say-so.
14
+
15
+ ## Features
16
+
17
+ - **9 tools**: `read` · `ls` · `glob` · `grep` · `webfetch` · `write` · `edit` · `bash` · `task` (subagent)
18
+ - **Plan & Build** modes — explore read-only, switch to full access when ready
19
+ - **Permissioned by design** — allow / ask / deny enforced in code, not in the prompt; dangerous commands auto-blocked (`rm -rf`, `sudo`, `curl | sh`)
20
+ - **38 skills** — auto-detected by keyword matching, or invoked explicitly via `/skill-name`; full Agent Skills format support
21
+ - **Subagents** — delegate complex tasks to isolated agents (`explore` for read-only, `general` for full access)
22
+ - **Atomic edit** — unique-match string replacement with `replaceAll` support
23
+ - **Safe bash** — timeout, output truncation, exit code, dangerous-command deny list
24
+ - **Session persistence** — messages saved per-project, resume with `--continue`; `/sessions` picker
25
+ - **Undo / redo** — file snapshots before every mutation; `/undo` `/redo`
26
+ - **Slash commands** — `/help` `/clear` `/init` `/model` `/plan` `/build` `/undo` `/redo` `/compact` `/sessions` `/rename` `/provider` `/thinking`
27
+ - **`/init`** — analyzes your project and generates `AGENTS.md`
28
+ - **`/provider`** — manage API keys interactively (stored in `~/.interference/auth.json`)
29
+ - **Skill invocation** — explicit `/skill-name` + automatic keyword matching on description
30
+ - **Context compaction** — auto-summarizes conversation at ~90% context limit
31
+ - **Config file** — per-project `interference.json` (model, permissions, mode, instructions)
32
+ - **Diff view** — color-coded (+/-, green/red) in TUI for every edit/write
33
+ - **TUI with Ink** — `<Static>` history, streaming, spinner, TextInput, status footer (model / mode / context% / cost / git branch), pickers (model, provider, thinking), slash autocomplete, session list, toast, welcome screen
34
+ - **Multi-provider** — DeepSeek, OpenAI (GPT-5.5), Anthropic (Claude), Zhipu (GLM), Moonshot (Kimi) + any OpenAI-compatible endpoint
35
+ - **Reasoning/thinking** — distinct `┄ thinking` blocks for every provider, enabled at max
36
+ - **Cost tracking** — real-time cost estimation per model
37
+ - **AGENTS.md & CLAUDE.md** — auto-loaded from project tree into system prompt
38
+ - **Italian** — made in Italy, MIT licensed, European
39
+
40
+ ## Why
41
+
42
+ - **Terminal-native** — no editor lock-in, no web UI; just your shell
43
+ - **Permissioned by design** — allow / ask / deny enforced in code
44
+ - **European / by choice** — Italian, MIT, GDPR-native, no vendor lock-in
45
+ - **Radically transparent** — every tool call, reasoning step, and API cost shown live
46
+
47
+ ## Stack
48
+
49
+ [Bun](https://bun.sh) · TypeScript · [Vercel AI SDK](https://ai-sdk.dev) (`ai` v7) · [zod](https://zod.dev) · [Ink 7.1](https://github.com/vadimdemedes/ink) + React 19.2 (TUI)
50
+
51
+ ## Quickstart
52
+
53
+ ```bash
54
+ bun install
55
+ # Set API key in .env (Bun auto-loads it)
56
+ echo 'DEEPSEEK_API_KEY=sk-...' > .env
57
+ bun run interference
58
+ ```
59
+
60
+ > Requires **Bun 1.3+** (Node ≥22 / React ≥19.2 for the Ink TUI).
61
+
62
+ ## Screenshot
63
+
64
+ ![interference CLI](assets/screenshot.png)
65
+
66
+ *(Capture your terminal with Cmd+Shift+4, save as `assets/screenshot.png`)*
67
+
68
+ ## Landing page
69
+
70
+ A static landing page lives in [`site/`](site/).
71
+
72
+ ## License
73
+
74
+ [MIT](LICENSE)
Binary file
package/bun.lock ADDED
@@ -0,0 +1,159 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "interference",
7
+ "dependencies": {
8
+ "@ai-sdk/anthropic": "^4.0.1",
9
+ "@ai-sdk/deepseek": "^3.0.1",
10
+ "@ai-sdk/openai-compatible": "^3.0.1",
11
+ "@inkjs/ui": "^2.0.0",
12
+ "ai": "^7.0.4",
13
+ "ink": "^7.1.0",
14
+ "ink-text-input": "^6.0.0",
15
+ "react": "^19.2.7",
16
+ "zod": "^4.4.3",
17
+ },
18
+ "devDependencies": {
19
+ "@types/bun": "latest",
20
+ "@types/react": "^19.2.17",
21
+ "ink-testing-library": "^4.0.0",
22
+ "typescript": "^5.6.0",
23
+ },
24
+ },
25
+ },
26
+ "packages": {
27
+ "@ai-sdk/anthropic": ["@ai-sdk/anthropic@4.0.1", "", { "dependencies": { "@ai-sdk/provider": "4.0.0", "@ai-sdk/provider-utils": "5.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-q9PMy0r3LGzSTu6FtYhFj6EesVpSpsEoPLsRKFnaK4YJd7SdUJlYIUJ4EXd5LTWzeXxk4r02z3gymo24c9jyQA=="],
28
+
29
+ "@ai-sdk/deepseek": ["@ai-sdk/deepseek@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "4.0.0", "@ai-sdk/provider-utils": "5.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-isMBNuG8wWWZ8FvQ76Y8sp8L0ET6GCqD9WydRUA8WkcWJ/7PGnli6kE2fkfFVJF4fYS0JcAdLuxVnoTnngTT5w=="],
30
+
31
+ "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.4", "", { "dependencies": { "@ai-sdk/provider": "4.0.0", "@ai-sdk/provider-utils": "5.0.1", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-xO0e9duft/uZlnDaIeBZmzTMjfdEz1kkDzWnhQNkJZZljsKWVi6IREZW9nAa+Dx6jYJssyxSUggaFYBAV1WZpw=="],
32
+
33
+ "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "4.0.0", "@ai-sdk/provider-utils": "5.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nUbQoGzdFZspb1cr6GUy+nR8Uvdi4/lCwTkdE9qKe+Qr2OVPsIKRIpyC9ewJ2uqUfl+1JlkWhPfR5l8bokNs4Q=="],
34
+
35
+ "@ai-sdk/provider": ["@ai-sdk/provider@4.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-fr9Gs89prDWiuox/T+kCA+i2cJkHpxU5S+tr4megjTzRC27ZsvFhwjU/+XrqqMbvBUlfmXxTOYWy8ng45dsjIg=="],
36
+
37
+ "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.1", "", { "dependencies": { "@ai-sdk/provider": "4.0.0", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-p9Ra+dN4jjHrssXvklNf4nFvWbj1KePMfUOs7nue0NuoIMbYFBULhX4Vu0+6DWLnw3+UsLL9+RCKLtzzU43Qpg=="],
38
+
39
+ "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="],
40
+
41
+ "@inkjs/ui": ["@inkjs/ui@2.0.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-spinners": "^3.0.0", "deepmerge": "^4.3.1", "figures": "^6.1.0" }, "peerDependencies": { "ink": ">=5" } }, "sha512-5+8fJmwtF9UvikzLfph9sA+LS+l37Ij/szQltkuXLOAXwNkBX9innfzh4pLGXIB59vKEQUtc6D4qGvhD7h3pAg=="],
42
+
43
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
44
+
45
+ "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
46
+
47
+ "@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="],
48
+
49
+ "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
50
+
51
+ "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="],
52
+
53
+ "@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="],
54
+
55
+ "ai": ["ai@7.0.4", "", { "dependencies": { "@ai-sdk/gateway": "4.0.4", "@ai-sdk/provider": "4.0.0", "@ai-sdk/provider-utils": "5.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-mZub080RWRxpWg8OY56KVnI3AoMVniccSWG+dwVb3nH81+GiNMDYBIdP41RLUivLpmU49pB7ssD7tvfIUaSaCA=="],
56
+
57
+ "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
58
+
59
+ "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
60
+
61
+ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
62
+
63
+ "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
64
+
65
+ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
66
+
67
+ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
68
+
69
+ "cli-boxes": ["cli-boxes@4.0.1", "", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="],
70
+
71
+ "cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
72
+
73
+ "cli-spinners": ["cli-spinners@3.4.0", "", {}, "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw=="],
74
+
75
+ "cli-truncate": ["cli-truncate@6.1.0", "", { "dependencies": { "slice-ansi": "^9.0.0", "string-width": "^8.2.0" } }, "sha512-ofWtXdvPO1WepoE9Gn4dPdZw+ADud1Yz9K+xm9FjK5vvrs/D60vrlKIQeSw1wJX4cphW2bnWi8GZSKvf9T6shw=="],
76
+
77
+ "code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
78
+
79
+ "convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
80
+
81
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
82
+
83
+ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
84
+
85
+ "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
86
+
87
+ "es-toolkit": ["es-toolkit@1.49.0", "", {}, "sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g=="],
88
+
89
+ "escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
90
+
91
+ "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="],
92
+
93
+ "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
94
+
95
+ "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
96
+
97
+ "indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
98
+
99
+ "ink": ["ink@7.1.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.3", "auto-bind": "^5.0.1", "chalk": "^5.6.2", "cli-boxes": "^4.0.1", "cli-cursor": "^4.0.0", "cli-truncate": "^6.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.45.1", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^9.0.0", "stack-utils": "^2.0.6", "string-width": "^8.2.0", "terminal-size": "^4.0.1", "type-fest": "^5.5.0", "widest-line": "^6.0.0", "wrap-ansi": "^10.0.0", "ws": "^8.20.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.2.0", "react": ">=19.2.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-VWE6/yeLtFCJBNLflyI2OSylyXK1Rc24LuXup8Qt+icwkmmycFNdbn8IkSp6Frc0h1iA0NOvvi1ajW44U/w3Qg=="],
100
+
101
+ "ink-testing-library": ["ink-testing-library@4.0.0", "", { "peerDependencies": { "@types/react": ">=18.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-yF92kj3pmBvk7oKbSq5vEALO//o7Z9Ck/OaLNlkzXNeYdwfpxMQkSowGTFUCS5MSu9bWfSZMewGpp7bFc66D7Q=="],
102
+
103
+ "ink-text-input": ["ink-text-input@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "type-fest": "^4.18.2" }, "peerDependencies": { "ink": ">=5", "react": ">=18" } }, "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw=="],
104
+
105
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
106
+
107
+ "is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="],
108
+
109
+ "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
110
+
111
+ "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
112
+
113
+ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
114
+
115
+ "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
116
+
117
+ "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
118
+
119
+ "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
120
+
121
+ "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="],
122
+
123
+ "restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
124
+
125
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
126
+
127
+ "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
128
+
129
+ "slice-ansi": ["slice-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-SO/3iYL5S3W57LLEniscOGPZgOqZUPCx6d3dB+52B80yJ0XstzsC/eV8gnA4tM3MHDrKz+OCFSLNjswdSC+/bA=="],
130
+
131
+ "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
132
+
133
+ "string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
134
+
135
+ "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
136
+
137
+ "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
138
+
139
+ "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="],
140
+
141
+ "type-fest": ["type-fest@5.7.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg=="],
142
+
143
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
144
+
145
+ "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
146
+
147
+ "widest-line": ["widest-line@6.0.0", "", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="],
148
+
149
+ "wrap-ansi": ["wrap-ansi@10.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="],
150
+
151
+ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
152
+
153
+ "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
154
+
155
+ "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
156
+
157
+ "ink-text-input/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
158
+ }
159
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "interference-agent",
3
+ "version": "0.1.0",
4
+ "description": "The open-source coding agent that lives in your terminal.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "ricciviero",
8
+ "repository": "github:ricciviero/interference",
9
+ "homepage": "https://github.com/ricciviero/interference#readme",
10
+ "keywords": ["ai", "coding-agent", "terminal", "cli", "bun", "typescript", "llm", "open-source"],
11
+ "bin": {
12
+ "interference": "./src/cli.ts"
13
+ },
14
+ "scripts": {
15
+ "interference": "bun run src/cli.ts",
16
+ "start": "bun run src/cli.ts",
17
+ "dev": "bun run --watch src/cli.ts",
18
+ "typecheck": "bunx tsc --noEmit",
19
+ "build": "bun build src/cli.ts --compile --minify --sourcemap --outfile dist/interference",
20
+ "test": "bun test ./src"
21
+ },
22
+ "dependencies": {
23
+ "@ai-sdk/anthropic": "^4.0.1",
24
+ "@ai-sdk/deepseek": "^3.0.1",
25
+ "@ai-sdk/openai-compatible": "^3.0.1",
26
+ "@inkjs/ui": "^2.0.0",
27
+ "ai": "^7.0.4",
28
+ "ink": "^7.1.0",
29
+ "ink-text-input": "^6.0.0",
30
+ "react": "^19.2.7",
31
+ "zod": "^4.4.3"
32
+ },
33
+ "devDependencies": {
34
+ "@types/bun": "latest",
35
+ "@types/react": "^19.2.17",
36
+ "ink-testing-library": "^4.0.0",
37
+ "typescript": "^5.6.0"
38
+ }
39
+ }
@@ -0,0 +1,114 @@
1
+ import { generateText, type ModelMessage } from "ai";
2
+ import { resolveModel } from "../provider.ts";
3
+ import { currentProvider } from "../config.ts";
4
+
5
+ const COMPACT_THRESHOLD = 0.9;
6
+ const DEFAULT_CONTEXT = 200_000;
7
+
8
+ function getContextLimit(): number {
9
+ return currentProvider().contextLimit ?? DEFAULT_CONTEXT;
10
+ }
11
+
12
+ function estimateTokens(text: string): number {
13
+ return Math.ceil(text.length / 3.5);
14
+ }
15
+
16
+ export function estimateMessagesTokens(messages: ModelMessage[]): number {
17
+ let total = 0;
18
+ for (const m of messages) {
19
+ if (typeof m.content === "string") {
20
+ total += estimateTokens(m.content);
21
+ } else if (Array.isArray(m.content)) {
22
+ for (const part of m.content as Array<{ text?: string; type: string }>) {
23
+ if (part.text) total += estimateTokens(part.text);
24
+ }
25
+ }
26
+ }
27
+ return total;
28
+ }
29
+
30
+ export function shouldCompact(messages: ModelMessage[]): boolean {
31
+ const limit = getContextLimit();
32
+ const used = estimateMessagesTokens(messages);
33
+ return used > limit * COMPACT_THRESHOLD;
34
+ }
35
+
36
+ export function getUsagePercent(messages: ModelMessage[]): number {
37
+ const limit = getContextLimit();
38
+ const used = estimateMessagesTokens(messages);
39
+ return Math.round((used / limit) * 100);
40
+ }
41
+
42
+ export async function compactMessages(
43
+ messages: ModelMessage[],
44
+ preserveRecentTurns = 2,
45
+ ): Promise<ModelMessage[]> {
46
+ if (messages.length === 0) return messages;
47
+
48
+ const userIndices: number[] = [];
49
+ for (let i = 0; i < messages.length; i++) {
50
+ if (messages[i]!.role === "user") userIndices.push(i);
51
+ }
52
+
53
+ if (userIndices.length <= preserveRecentTurns) return messages;
54
+
55
+ const splitIndex = userIndices[userIndices.length - preserveRecentTurns]!;
56
+ const head = messages.slice(0, splitIndex);
57
+ const tail = messages.slice(splitIndex);
58
+
59
+ const summary = await generateSummary(head);
60
+ if (!summary) return messages;
61
+
62
+ const compacted: ModelMessage[] = [
63
+ {
64
+ role: "user",
65
+ content: `<compacted_summary>\n${summary}\n</compacted_summary>\n\nContinue from where you left off.`,
66
+ },
67
+ {
68
+ role: "assistant",
69
+ content: "I'll continue with the remaining context.",
70
+ },
71
+ ...tail,
72
+ ];
73
+
74
+ return compacted;
75
+ }
76
+
77
+ async function generateSummary(
78
+ messages: ModelMessage[],
79
+ ): Promise<string | null> {
80
+ const serialized = serializeMessages(messages);
81
+ if (serialized.length < 100) return null;
82
+
83
+ const prompt = `Summarize the following conversation between a user and an AI coding agent.
84
+ Focus on: what was done, which files were modified, key decisions made, and what remains to do.
85
+ Be concise but comprehensive. Write in the same language as the conversation.
86
+
87
+ <conversation>
88
+ ${serialized.slice(0, 16000)}
89
+ </conversation>
90
+
91
+ Return ONLY the summary, no preamble.`;
92
+
93
+ try {
94
+ const result = await generateText({
95
+ model: resolveModel(),
96
+ prompt,
97
+ maxOutputTokens: 2000,
98
+ });
99
+ return result.text.trim() || null;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ function serializeMessages(messages: ModelMessage[]): string {
106
+ const lines: string[] = [];
107
+ for (const m of messages) {
108
+ const role = m.role === "user" ? "User" : m.role === "assistant" ? "Agent" : m.role;
109
+ const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
110
+ const truncated = content.length > 2000 ? content.slice(0, 2000) + "…" : content;
111
+ lines.push(`[${role}]: ${truncated}`);
112
+ }
113
+ return lines.join("\n");
114
+ }
@@ -0,0 +1,94 @@
1
+ import { streamText, stepCountIs, type ModelMessage } from "ai";
2
+ import { resolveModel } from "../provider.ts";
3
+ import { currentMode, reasoningConfig, type AgentMode } from "../config.ts";
4
+ import { systemPrompt } from "./prompt.ts";
5
+ import { toolsForMode } from "../tools/index.ts";
6
+ import { trackUsage } from "../cost.ts";
7
+
8
+ export type Chunk =
9
+ | { type: "text" | "reasoning"; text: string }
10
+ | { type: "tool-call"; toolName: string; input: unknown }
11
+ | { type: "tool-result"; toolName: string; output: string; isError: boolean };
12
+
13
+ export async function* runTurn(
14
+ messages: ModelMessage[],
15
+ signal?: AbortSignal,
16
+ mode?: AgentMode,
17
+ skillBodies?: string[],
18
+ overrideSystem?: string,
19
+ ): AsyncGenerator<Chunk> {
20
+ const reasoning = reasoningConfig();
21
+ const effectiveMode = mode ?? currentMode();
22
+ const tools = toolsForMode(effectiveMode);
23
+
24
+ let system = overrideSystem ?? systemPrompt(effectiveMode);
25
+ if (skillBodies && skillBodies.length > 0) {
26
+ system += "\n\n<skill_context>\n" + skillBodies.join("\n\n---\n\n") + "\n</skill_context>";
27
+ }
28
+
29
+ const result = streamText({
30
+ model: resolveModel(),
31
+ system,
32
+ messages,
33
+ tools,
34
+ stopWhen: stepCountIs(20),
35
+ abortSignal: signal,
36
+ onError: () => {},
37
+ ...(reasoning.providerOptions
38
+ ? {
39
+ providerOptions: reasoning.providerOptions as Parameters<
40
+ typeof streamText
41
+ >[0]["providerOptions"],
42
+ }
43
+ : {}),
44
+ ...(reasoning.maxOutputTokens ? { maxOutputTokens: reasoning.maxOutputTokens } : {}),
45
+ });
46
+
47
+ for await (const part of result.fullStream) {
48
+ switch (part.type) {
49
+ case "text-delta":
50
+ yield { type: "text", text: part.text };
51
+ break;
52
+
53
+ case "reasoning-delta":
54
+ yield { type: "reasoning", text: part.text };
55
+ break;
56
+
57
+ case "tool-call":
58
+ yield { type: "tool-call", toolName: part.toolName, input: part.input };
59
+ break;
60
+
61
+ case "tool-result": {
62
+ const tr = part as unknown as {
63
+ toolName: string;
64
+ output: unknown;
65
+ error?: unknown;
66
+ };
67
+ const err = tr.error;
68
+ const out = tr.output;
69
+ yield {
70
+ type: "tool-result",
71
+ toolName: tr.toolName,
72
+ output: err
73
+ ? String(err)
74
+ : typeof out === "string"
75
+ ? out
76
+ : JSON.stringify(out),
77
+ isError: !!err,
78
+ };
79
+ break;
80
+ }
81
+
82
+ case "error":
83
+ throw part.error;
84
+ }
85
+ }
86
+
87
+ const response = await result.response;
88
+ messages.push(...response.messages);
89
+
90
+ const usage = await result.usage;
91
+ if (usage) {
92
+ trackUsage(usage.inputTokens ?? 0, usage.outputTokens ?? 0);
93
+ }
94
+ }
@@ -0,0 +1,89 @@
1
+ import { loadInstructions, formatInstructionBlock, type InstructionBlock } from "../context.ts";
2
+ import { loadSkillRegistry, bootstrapSkills } from "../skills.ts";
3
+
4
+ let cachedInstructions: InstructionBlock[] | null = null;
5
+ let skillsSummary: string | null = null;
6
+
7
+ export async function initInstructions(): Promise<InstructionBlock[]> {
8
+ cachedInstructions = await loadInstructions();
9
+ const registry = await loadSkillRegistry();
10
+ if (registry.length > 0) {
11
+ skillsSummary = "Available skills (use /<name> or trigger by description):\n" +
12
+ registry.map((s) => `- \`${s.name}\`: ${s.description}`).join("\n");
13
+ }
14
+ return cachedInstructions;
15
+ }
16
+
17
+ export function systemPrompt(mode: "plan" | "build", instructions?: InstructionBlock[]): string {
18
+ const blocks = instructions ?? cachedInstructions ?? [];
19
+ const instructionText = blocks.length > 0
20
+ ? "\n<instructions>\n" + blocks.map(formatInstructionBlock).join("\n\n") + "\n</instructions>\n"
21
+ : "";
22
+
23
+ const skillsText = skillsSummary
24
+ ? "\n<available_skills>\n" + skillsSummary + "\n</available_skills>\n"
25
+ : "";
26
+
27
+ const envSection = `Working directory: ${process.cwd()}
28
+ OS: ${process.platform}
29
+ Date: ${new Date().toISOString().split("T")[0]}`;
30
+
31
+ const base = `You are interference, an AI coding agent running in the user's terminal.
32
+
33
+ <environment>
34
+ ${envSection}
35
+ </environment>${instructionText}${skillsText}
36
+ You have access to these tools:
37
+ - read: read file contents with line numbers (use offset/limit for large files)
38
+ - ls: list files and directories
39
+ - glob: find files by pattern (e.g. "src/**/*.ts")
40
+ - grep: search file contents with regex
41
+ - webfetch: fetch a URL and return its text content (for documentation, API references, research)
42
+ - todowrite: maintain a structured task list. Use for multi-step work (3+ distinct steps):
43
+ create the plan up front, then update statuses in real time. Keep exactly ONE task
44
+ 'in_progress' at a time and mark tasks 'completed' as soon as they are done. Skip it
45
+ for trivial single-step tasks.
46
+ - question: ask the user one or more multiple-choice questions when a decision is genuinely
47
+ ambiguous and you cannot resolve it from the request, the code, or sensible defaults.
48
+ Execution pauses until the user answers. Use sparingly; prefer sensible defaults.`;
49
+
50
+ if (mode === "build") {
51
+ return (
52
+ base +
53
+ `
54
+ - write: create or overwrite a file
55
+ - edit: replace a string in a file. The oldString must match EXACTLY ONCE in the file.
56
+ Use 'replaceAll: true' to replace all occurrences.
57
+ Prefer edit over write for targeted changes. If oldString matches multiple
58
+ times, add more surrounding context to make it unique.
59
+ - bash: execute a shell command. Use for git, tests, build, package management.
60
+ NEVER use interactive commands (no -i flag, no vim/nano). Commands that
61
+ may be destructive (rm, sudo, curl pipe, force push) are blocked.
62
+ - task: launch a subagent for complex multi-step tasks (types: 'explore' for read-only,
63
+ 'general' for full access). Use when a task requires isolated context.
64
+ - webfetch: fetch content from a URL (HTML is stripped to text). Use for documentation,
65
+ API references, or researching external resources.
66
+
67
+ Rules:
68
+ - Be concise and precise. Prefer short, direct answers; expand only when asked.
69
+ - Use edit for small changes, write only for new files or complete rewrites.
70
+ - Before using bash, explain what the command will do.
71
+ - After editing a file, the user may need to approve the change.
72
+ - When you are unsure, say so instead of guessing.
73
+ - Never use emojis in responses.
74
+ - Format code in fenced blocks with the right language tag.`
75
+ );
76
+ }
77
+
78
+ return (
79
+ base +
80
+ `
81
+ You are running in Plan mode (read-only). You cannot modify files or execute commands.
82
+ - Be concise and precise. Prefer short, direct answers; expand only when asked.
83
+ - When exploring the codebase: use ls/glob to map structure, grep to find code, read to inspect.
84
+ - Answer with specific file:line references.
85
+ - When you are unsure, say so instead of guessing.
86
+ - Never use emojis in responses.
87
+ - Format code in fenced blocks with the right language tag.`
88
+ );
89
+ }