loki-mode 11.1.0 → 11.2.1

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 (91) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/lib/skill-link-heal.sh +54 -0
  5. package/autonomy/loki +60 -6
  6. package/autonomy/run.sh +11 -3
  7. package/bin/loki +8 -0
  8. package/bin/loki-postinstall.js +3 -1
  9. package/dashboard/__init__.py +1 -1
  10. package/docs/CLI-REFERENCE.md +1 -1
  11. package/docs/INSTALLATION.md +1 -1
  12. package/docs/WANG-PRINCIPLES-PLAN.md +1 -1
  13. package/docs/v10/DECISIONS.md +4 -0
  14. package/docs/v10/FAILURE-CLASSES.md +27 -1
  15. package/docs/v10/GUIDE.md +85 -0
  16. package/docs/v11/ROUTER-1.md +15 -13
  17. package/loki-ts/dist/loki.js +687 -680
  18. package/mcp/__init__.py +1 -1
  19. package/mcp/cp_tools.py +333 -0
  20. package/mcp/server.py +21 -0
  21. package/mcp/tests/_read_only_wall_probe.py +10 -0
  22. package/mcp/tests/fixtures/cp_api/artifact_report.md +3 -0
  23. package/mcp/tests/fixtures/cp_api/cost.json +1 -0
  24. package/mcp/tests/fixtures/cp_api/repos.json +1 -0
  25. package/mcp/tests/fixtures/cp_api/run_alpha.json +1 -0
  26. package/mcp/tests/fixtures/cp_api/run_beta.json +1 -0
  27. package/mcp/tests/fixtures/cp_api/run_events.json +1 -0
  28. package/mcp/tests/fixtures/cp_api/runs_list.json +1 -0
  29. package/mcp/tests/fixtures/cp_api/stats.json +1 -0
  30. package/mcp/tests/test_cp_tools.py +290 -0
  31. package/mcp/tests/test_read_only.py +6 -3
  32. package/package.json +1 -1
  33. package/packages/control-plane/dist/server.js +252 -65
  34. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  35. package/web-app/dist/assets/{AdminPage-Bqx993m_.js → AdminPage-BZ1QNSju.js} +1 -1
  36. package/web-app/dist/assets/{Avatar-BHjV5Csv.js → Avatar-Dnzzsk2U.js} +1 -1
  37. package/web-app/dist/assets/{Badge-DGbSCV8L.js → Badge-DWtsAw6M.js} +1 -1
  38. package/web-app/dist/assets/{Button-CnCZf-0w.js → Button-zEMFjJEr.js} +1 -1
  39. package/web-app/dist/assets/{CockpitPage-CyZC0Q4e.js → CockpitPage-Ddo5Zhnz.js} +1 -1
  40. package/web-app/dist/assets/{ComparePage-REwWeDu9.js → ComparePage-kCkyRrrz.js} +1 -1
  41. package/web-app/dist/assets/{ErrorBoundary-C0sG0fj4.js → ErrorBoundary-BZrVi8Ui.js} +1 -1
  42. package/web-app/dist/assets/{EvidenceReceiptPanel-nXcOrxiA.js → EvidenceReceiptPanel-B9eDIWUr.js} +1 -1
  43. package/web-app/dist/assets/{GitHubIssuesPanel-D8CUAA7b.js → GitHubIssuesPanel-DX5A2UkV.js} +1 -1
  44. package/web-app/dist/assets/{GitHubPRsPanel-B68Bf309.js → GitHubPRsPanel-DKKxBe6-.js} +1 -1
  45. package/web-app/dist/assets/{HomePage-mgJEeXH_.js → HomePage-BSn_-fHq.js} +2 -2
  46. package/web-app/dist/assets/{LoginPage-TM_L3-ws.js → LoginPage-C1BGwj0s.js} +1 -1
  47. package/web-app/dist/assets/{MagicPage-CoE5Yz73.js → MagicPage-B9JJw_BQ.js} +1 -1
  48. package/web-app/dist/assets/{MetricsPage-BubxRsx4.js → MetricsPage-B-lNQr8Q.js} +1 -1
  49. package/web-app/dist/assets/{NotFoundPage-udeE-1w6.js → NotFoundPage-DkktBNvI.js} +1 -1
  50. package/web-app/dist/assets/{ProjectPage-BuYFdDIy.js → ProjectPage-DUAraQw9.js} +3 -3
  51. package/web-app/dist/assets/{ProjectsPage-DP4T7hAW.js → ProjectsPage-DgMo4QHm.js} +1 -1
  52. package/web-app/dist/assets/{SettingsPage-fIgNrA9e.js → SettingsPage-fnL3nqGC.js} +1 -1
  53. package/web-app/dist/assets/{ShowcasePage-9lILj-je.js → ShowcasePage-Bn7rWTXn.js} +1 -1
  54. package/web-app/dist/assets/{SystemSettingsPage-BYBBdIyC.js → SystemSettingsPage-DxrytIXT.js} +1 -1
  55. package/web-app/dist/assets/{TeamsPage-qhHtMpzb.js → TeamsPage-BQDC12Mn.js} +1 -1
  56. package/web-app/dist/assets/{TemplatesPage-DIClsS3d.js → TemplatesPage-C_JEygcZ.js} +1 -1
  57. package/web-app/dist/assets/{TerminalOutput-DKczDGbC.js → TerminalOutput-H0584wn0.js} +1 -1
  58. package/web-app/dist/assets/{activity-CU5PK4CB.js → activity-Bse7FP2H.js} +1 -1
  59. package/web-app/dist/assets/{bell-Czwm7c23.js → bell-BMtgCGSW.js} +1 -1
  60. package/web-app/dist/assets/{bot-BNB-olYj.js → bot-6SW_XTZN.js} +1 -1
  61. package/web-app/dist/assets/{check-DEmFNYuC.js → check-C3f9aTHx.js} +1 -1
  62. package/web-app/dist/assets/{chevron-left-DcSPJ4KZ.js → chevron-left-Bi60XjBc.js} +1 -1
  63. package/web-app/dist/assets/{circle-alert-CQ0-gHsx.js → circle-alert-Bepfb6dn.js} +1 -1
  64. package/web-app/dist/assets/{clock-CvLXmhWQ.js → clock-jQIU5AuJ.js} +1 -1
  65. package/web-app/dist/assets/{cloud-6BuQFMTx.js → cloud-BLZn6J9A.js} +1 -1
  66. package/web-app/dist/assets/{code-xml-BMf3egg0.js → code-xml-CRgKi92z.js} +1 -1
  67. package/web-app/dist/assets/{database-BBTbPQOn.js → database-DWg9XT6Y.js} +1 -1
  68. package/web-app/dist/assets/{dollar-sign-B8vHgGQG.js → dollar-sign-Cfif5nlf.js} +1 -1
  69. package/web-app/dist/assets/{file-code-corner-DwAQcHOd.js → file-code-corner-DSiLXED8.js} +1 -1
  70. package/web-app/dist/assets/{file-plus-BlhGOB-I.js → file-plus-8lIzYfAS.js} +1 -1
  71. package/web-app/dist/assets/{globe-6a-K3mZa.js → globe-DZkvR077.js} +1 -1
  72. package/web-app/dist/assets/{hammer-CU_u8tsP.js → hammer-BtQyfpf_.js} +1 -1
  73. package/web-app/dist/assets/{index-BN6SdA1r.js → index-B-uQiPRR.js} +3 -3
  74. package/web-app/dist/assets/{layers-VJW1LqHm.js → layers-Dlfo3jwV.js} +1 -1
  75. package/web-app/dist/assets/{loader-circle-DfL0cxR3.js → loader-circle-BQ2hL7m5.js} +1 -1
  76. package/web-app/dist/assets/{lock-D2LRX42k.js → lock-LzpJKEAP.js} +1 -1
  77. package/web-app/dist/assets/{package-_JNG_ljQ.js → package-zp-qZxpp.js} +1 -1
  78. package/web-app/dist/assets/{plus-qYicH17Y.js → plus-CuiDkGsm.js} +1 -1
  79. package/web-app/dist/assets/{refresh-cw-Bqb0Vbp-.js → refresh-cw-Dx57uk3-.js} +1 -1
  80. package/web-app/dist/assets/{rotate-ccw-Blheo-H1.js → rotate-ccw-Bp1YOkM6.js} +1 -1
  81. package/web-app/dist/assets/{scroll-text-3OO4nVP7.js → scroll-text-Dc8LjRT5.js} +1 -1
  82. package/web-app/dist/assets/{server-34C8ObzM.js → server-CE0Mzyfz.js} +1 -1
  83. package/web-app/dist/assets/{shield-alert-D7-nkuq3.js → shield-alert-DBj71EDP.js} +1 -1
  84. package/web-app/dist/assets/{thumbs-up-CvfrKMma.js → thumbs-up-C0q9J-0y.js} +1 -1
  85. package/web-app/dist/assets/{trash-2-4bt3fZC1.js → trash-2-YNPSnz_h.js} +1 -1
  86. package/web-app/dist/assets/{trending-down-C7Uu-rJL.js → trending-down-BlyQB_Gh.js} +1 -1
  87. package/web-app/dist/assets/{trending-up-B7TF-ynr.js → trending-up-CStScgAm.js} +1 -1
  88. package/web-app/dist/assets/{upload-YlZG_g5B.js → upload-CLs-rKu9.js} +1 -1
  89. package/web-app/dist/assets/{usePolling-BDZJZdnI.js → usePolling-BmFkptqQ.js} +1 -1
  90. package/web-app/dist/assets/{user-C2Z3KTt0.js → user-CV0TBz6A.js} +1 -1
  91. package/web-app/dist/index.html +1 -1
@@ -0,0 +1,290 @@
1
+ """CP-ASK slice 2: the CP data tools, driven only by fixtures (no live Control Plane)."""
2
+
3
+ import builtins
4
+ import json
5
+ import os
6
+ import re
7
+ import sys
8
+ import tempfile
9
+ import unittest
10
+ import urllib.request
11
+ from unittest import mock
12
+
13
+ _REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
14
+ if _REPO_ROOT not in sys.path:
15
+ sys.path.insert(0, _REPO_ROOT)
16
+
17
+ from mcp import cp_tools # noqa: E402
18
+
19
+ FIX = os.path.join(os.path.dirname(__file__), "fixtures", "cp_api")
20
+ URL = "http://127.0.0.1:7777"
21
+ TOKEN = "s3cret-token-value"
22
+
23
+
24
+ def _fx(name):
25
+ with open(os.path.join(FIX, name), "rb") as f:
26
+ return f.read()
27
+
28
+
29
+ ROUTES = {
30
+ "/v1/runs": "runs_list.json",
31
+ "/v1/runs/src-a/e10-aaa111": "run_alpha.json",
32
+ "/v1/runs/src-b/e10-bbb222": "run_beta.json",
33
+ "/v1/runs/src-a/e10-aaa111/events": "run_events.json",
34
+ "/v1/runs/src-a/e10-aaa111/artifact/report.md": "artifact_report.md",
35
+ "/v1/stats": "stats.json",
36
+ "/v1/stats/cost": "cost.json",
37
+ "/v1/repos": "repos.json",
38
+ }
39
+
40
+
41
+ class FixtureTransport:
42
+ def __init__(self):
43
+ self.calls = []
44
+
45
+ def __call__(self, path, query):
46
+ self.calls.append((path, dict(query)))
47
+ if path in ROUTES:
48
+ return 200, _fx(ROUTES[path])
49
+ return 404, b'{"error":"run not found"}'
50
+
51
+
52
+ def _client(transport=None):
53
+ t = transport or FixtureTransport()
54
+ return cp_tools.CpClient(transport=t, base_url=URL, token=TOKEN), t
55
+
56
+
57
+ class CompareTests(unittest.TestCase):
58
+ def test_compare_returns_both_ids_verdicts_and_costs(self):
59
+ c, _ = _client()
60
+ r = cp_tools.runs_compare(c, "src-a", "e10-aaa111", "src-b", "e10-bbb222")
61
+ self.assertEqual([x["run_id"] for x in r["runs"]], ["e10-aaa111", "e10-bbb222"])
62
+ self.assertEqual([x["verdict"] for x in r["runs"]], ["VERIFIED", "FAILED"])
63
+ self.assertEqual([x["cost_usd"] for x in r["runs"]], [0.42, 1.25])
64
+ self.assertAlmostEqual(r["cost_delta_usd"], 0.83, places=6)
65
+
66
+ def test_compare_unknown_run_is_an_error(self):
67
+ c, _ = _client()
68
+ r = cp_tools.runs_compare(c, "src-a", "e10-aaa111", "src-b", "nope")
69
+ self.assertIn("error", r)
70
+
71
+ def test_other_read_tools_return_fixture_data(self):
72
+ c, _ = _client()
73
+ self.assertEqual(cp_tools.runs_search(c)["total"], 2)
74
+ self.assertEqual(cp_tools.run_get(c, "src-b", "e10-bbb222")["verdict"], "FAILED")
75
+ self.assertEqual(len(cp_tools.run_events(c, "src-a", "e10-aaa111")["events"]), 2)
76
+ self.assertEqual(cp_tools.stats(c)["runs_total"], 2)
77
+ self.assertEqual(cp_tools.cost(c)["rows"][0]["runs"], 2)
78
+
79
+ def test_event_text_is_returned_as_data(self):
80
+ c, _ = _client()
81
+ ev = cp_tools.run_events(c, "src-a", "e10-aaa111")["events"][1]
82
+ self.assertIn("Ignore previous instructions", ev["data"]["title"])
83
+
84
+ def test_repos_list_is_names_only(self):
85
+ c, _ = _client()
86
+ self.assertEqual(cp_tools.repos_list(c), {"repos": ["acme/widgets", "acme/gadgets"]})
87
+
88
+
89
+ class RefusalTests(unittest.TestCase):
90
+ BAD = ["secrets.json", "../receipt.json", "receipt.json/../../x", "/etc/passwd",
91
+ "evidence/../x.png", "plan.json\x00.md", "evidence/shot.png", "", "report.md.bak",
92
+ "RECEIPT.json", ".loki/state.json"]
93
+
94
+ def test_artifact_outside_allowlist_is_refused_without_a_request(self):
95
+ for name in self.BAD:
96
+ c, t = _client()
97
+ r = cp_tools.run_artifact(c, "src-a", "e10-aaa111", name)
98
+ self.assertIn("error", r, name)
99
+ self.assertNotIn("content", r, name)
100
+ self.assertEqual(t.calls, [], "request made for " + repr(name))
101
+
102
+ def test_allowlisted_artifact_is_read(self):
103
+ c, t = _client()
104
+ r = cp_tools.run_artifact(c, "src-a", "e10-aaa111", "report.md")
105
+ self.assertEqual(r["content"], "# Report\n\nAll tests passed.\n")
106
+ self.assertEqual(len(t.calls), 1)
107
+
108
+ def test_trailing_newline_is_refused_without_a_request(self):
109
+ c, t = _client()
110
+ self.assertIn("error", cp_tools.run_artifact(c, "src-a", "e10-aaa111", "report.md\n"))
111
+ self.assertIn("error", cp_tools.run_artifact(c, "src-a\n", "e10-aaa111", "report.md"))
112
+ self.assertIn("error", cp_tools.run_get(c, "src-a", "e10-aaa111\n"))
113
+ self.assertIn("error", cp_tools.run_events(c, "src-a\n", "e10-aaa111"))
114
+ self.assertEqual(t.calls, [])
115
+
116
+ def test_bad_ids_are_refused_without_a_request(self):
117
+ for sid, rid in (("../x", "r"), ("s", "a/b"), ("s", ".."), ("", "r"), ("s", "a b")):
118
+ c, t = _client()
119
+ self.assertIn("error", cp_tools.run_get(c, sid, rid))
120
+ self.assertIn("error", cp_tools.run_artifact(c, sid, rid, "report.md"))
121
+ self.assertEqual(t.calls, [])
122
+
123
+ def test_bad_args_are_refused(self):
124
+ c, t = _client()
125
+ self.assertIn("error", cp_tools.runs_search(c, limit=0))
126
+ self.assertIn("error", cp_tools.runs_search(c, limit=True))
127
+ self.assertIn("error", cp_tools.runs_search(c, since="yesterday"))
128
+ self.assertIn("error", cp_tools.cost(c, group="day; drop"))
129
+ self.assertIn("error", cp_tools.run_events(c, "src-a", "e10-aaa111", after=-5))
130
+ self.assertEqual(t.calls, [])
131
+
132
+ def test_unconfigured_client_refuses(self):
133
+ with mock.patch.dict(os.environ, {}, clear=False):
134
+ for k in ("LOKI_CP_URL", "LOKI_CONTROL_URL"):
135
+ os.environ.pop(k, None)
136
+ c = cp_tools.CpClient()
137
+ self.assertIn("error", cp_tools.stats(c))
138
+
139
+
140
+ class RedactionTests(unittest.TestCase):
141
+ def _dump(self, *results):
142
+ return json.dumps(results)
143
+
144
+ def test_no_url_token_or_path_in_any_tool_output(self):
145
+ c, _ = _client()
146
+ outs = [
147
+ cp_tools.run_get(c, "src-a", "e10-aaa111"),
148
+ cp_tools.runs_compare(c, "src-a", "e10-aaa111", "src-b", "e10-bbb222"),
149
+ cp_tools.runs_search(c), cp_tools.stats(c), cp_tools.cost(c),
150
+ cp_tools.run_events(c, "src-a", "e10-aaa111"),
151
+ cp_tools.repos_list(c),
152
+ ]
153
+ blob = self._dump(*outs)
154
+ for secret in (TOKEN, URL, "127.0.0.1:7777", "/Users/someone/private"):
155
+ self.assertNotIn(secret, blob)
156
+ self.assertIn("[redacted]", json.dumps(outs[0]))
157
+
158
+ def test_transport_failure_does_not_leak_url_or_token(self):
159
+ def boom(path, query):
160
+ raise OSError("connect to %s failed with Bearer %s" % (URL, TOKEN))
161
+ c, _ = _client(boom)
162
+ r = cp_tools.run_get(c, "src-a", "e10-aaa111")
163
+ self.assertEqual(list(r), ["error"])
164
+ self.assertNotIn(TOKEN, json.dumps(r))
165
+ self.assertNotIn(URL, json.dumps(r))
166
+
167
+ def test_error_body_echoing_secrets_is_scrubbed(self):
168
+ def echo(path, query):
169
+ return 500, json.dumps({"error": "bad %s token %s" % (URL, TOKEN)}).encode()
170
+ c, _ = _client(echo)
171
+ blob = json.dumps(cp_tools.run_get(c, "src-a", "e10-aaa111"))
172
+ self.assertNotIn(TOKEN, blob)
173
+ self.assertNotIn(URL, blob)
174
+
175
+ def test_artifact_content_echoing_secrets_is_scrubbed(self):
176
+ def leak(path, query):
177
+ return 200, ("see %s/v1/runs using %s" % (URL, TOKEN)).encode()
178
+ c, _ = _client(leak)
179
+ blob = json.dumps(cp_tools.run_artifact(c, "src-a", "e10-aaa111", "report.md"))
180
+ self.assertNotIn(TOKEN, blob)
181
+ self.assertNotIn(URL, blob)
182
+
183
+ def test_redirect_is_never_followed(self):
184
+ h = cp_tools._NoRedirect()
185
+ self.assertIsNone(h.redirect_request(None, None, 302, "Found", {}, "http://evil.example/"))
186
+
187
+ def test_non_http_url_is_rejected(self):
188
+ c = cp_tools.CpClient(base_url="file:///etc", token=TOKEN)
189
+ r = cp_tools.stats(c)
190
+ self.assertEqual(list(r), ["error"])
191
+
192
+ def test_default_transport_sends_get_with_bearer(self):
193
+ seen = {}
194
+
195
+ class Resp:
196
+ status = 200
197
+ def __enter__(self): return self
198
+ def __exit__(self, *a): return False
199
+ def read(self, n=-1): return b'{"repos":[]}'
200
+
201
+ class Opener:
202
+ def open(self, req, timeout=None):
203
+ seen["method"] = req.get_method()
204
+ seen["auth"] = req.get_header("Authorization")
205
+ seen["url"] = req.full_url
206
+ return Resp()
207
+
208
+ with mock.patch.object(urllib.request, "build_opener", return_value=Opener()):
209
+ c = cp_tools.CpClient(base_url=URL, token=TOKEN)
210
+ r = cp_tools.repos_list(c)
211
+ self.assertEqual(r, {"repos": []})
212
+ self.assertEqual(seen["method"], "GET")
213
+ self.assertEqual(seen["auth"], "Bearer " + TOKEN)
214
+ self.assertTrue(seen["url"].startswith(URL + "/v1/repos"))
215
+
216
+
217
+ def _tree(root):
218
+ out = set()
219
+ for d, _dirs, files in os.walk(root):
220
+ for f in files:
221
+ p = os.path.join(d, f)
222
+ out.add((p, os.stat(p).st_mtime_ns, os.stat(p).st_size))
223
+ return out
224
+
225
+
226
+ class ReadOnlyTests(unittest.TestCase):
227
+ def test_tools_write_nothing_to_disk(self):
228
+ c, _ = _client()
229
+ real_open = builtins.open
230
+
231
+ def guarded(file, mode="r", *a, **k):
232
+ if any(ch in mode for ch in "wax+"):
233
+ raise AssertionError("write open: %r %r" % (file, mode))
234
+ return real_open(file, mode, *a, **k)
235
+
236
+ with tempfile.TemporaryDirectory() as tmp:
237
+ before = _tree(tmp)
238
+ old = os.getcwd()
239
+ os.chdir(tmp)
240
+ try:
241
+ with mock.patch.object(builtins, "open", guarded), \
242
+ mock.patch("os.mkdir", side_effect=AssertionError("mkdir")), \
243
+ mock.patch("os.remove", side_effect=AssertionError("remove")), \
244
+ mock.patch("subprocess.Popen", side_effect=AssertionError("spawn")):
245
+ cp_tools.runs_search(c)
246
+ cp_tools.run_get(c, "src-a", "e10-aaa111")
247
+ cp_tools.run_events(c, "src-a", "e10-aaa111")
248
+ cp_tools.run_artifact(c, "src-a", "e10-aaa111", "report.md")
249
+ cp_tools.runs_compare(c, "src-a", "e10-aaa111", "src-b", "e10-bbb222")
250
+ cp_tools.stats(c)
251
+ cp_tools.cost(c)
252
+ cp_tools.repos_list(c)
253
+ finally:
254
+ os.chdir(old)
255
+ self.assertEqual(_tree(tmp), before)
256
+
257
+ def test_every_request_is_a_get_of_a_read_route(self):
258
+ c, t = _client()
259
+ cp_tools.runs_compare(c, "src-a", "e10-aaa111", "src-b", "e10-bbb222")
260
+ cp_tools.run_artifact(c, "src-a", "e10-aaa111", "report.md")
261
+ for path, _q in t.calls:
262
+ self.assertTrue(path.startswith("/v1/"), path)
263
+ # compare is built from two run reads; the CP has no compare route
264
+ self.assertNotIn("/v1/compare", [p for p, _ in t.calls])
265
+
266
+ def test_module_has_no_write_primitives(self):
267
+ src = open(cp_tools.__file__).read()
268
+ for banned in ("subprocess", "shutil", "os.system", "method=\"POST\"", "method=\"PUT\"",
269
+ "method=\"DELETE\""):
270
+ self.assertNotIn(banned, src)
271
+ self.assertIsNone(re.search(r"(?<![.\w])open\(", src))
272
+
273
+
274
+ class SurfaceTests(unittest.TestCase):
275
+ def test_tool_names_match_registration(self):
276
+ names = []
277
+
278
+ class Fake:
279
+ def tool(self):
280
+ def deco(fn):
281
+ names.append(fn.__name__)
282
+ return fn
283
+ return deco
284
+
285
+ cp_tools.register_cp_tools(Fake())
286
+ self.assertEqual(tuple(names), cp_tools.TOOL_NAMES)
287
+
288
+
289
+ if __name__ == "__main__":
290
+ unittest.main()
@@ -18,6 +18,9 @@ EXPECTED_ALLOWLIST = {
18
18
  "loki_quality_report", "loki_code_search", "mem_search", "mem_timeline",
19
19
  "mem_get", "loki_get_hotspots", "loki_get_co_changes",
20
20
  "loki_get_doc_coverage", "loki_findings", "loki_learnings",
21
+ # CP data tools (CP-ASK slice 2)
22
+ "cp_runs_search", "cp_run_get", "cp_run_events", "cp_run_artifact",
23
+ "cp_runs_compare", "cp_stats", "cp_cost", "cp_repos_list",
21
24
  }
22
25
 
23
26
  # Tools that write, spawn processes, or are otherwise not on the allowlist.
@@ -104,7 +107,7 @@ class ReadOnlyTests(unittest.TestCase):
104
107
  full = _tools(False)
105
108
  self.assertTrue(EXPECTED_ALLOWLIST <= full)
106
109
  self.assertTrue(EXCLUDED_TOOLS <= full)
107
- self.assertGreaterEqual(len(full), 39)
110
+ self.assertGreaterEqual(len(full), 47)
108
111
 
109
112
  def test_resources_and_prompts_are_a_known_list(self):
110
113
  listing = _listing(True)
@@ -119,8 +122,8 @@ class ReadOnlyTests(unittest.TestCase):
119
122
  self.assertEqual(_main_tools([], value), EXPECTED_ALLOWLIST, value)
120
123
 
121
124
  def test_main_default_is_full_set(self):
122
- self.assertGreaterEqual(len(_main_tools([])), 39)
123
- self.assertGreaterEqual(len(_main_tools([], "0")), 39)
125
+ self.assertGreaterEqual(len(_main_tools([])), 47)
126
+ self.assertGreaterEqual(len(_main_tools([], "0")), 47)
124
127
 
125
128
 
126
129
  def _git(cwd, *args):
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "11.1.0",
4
+ "version": "11.2.1",
5
5
  "description": "An autonomous software factory that knows what it is supposed to deliver, and proves it did.",
6
6
  "keywords": [
7
7
  "agent",