cortad 0.3.0-rc.3 → 0.3.0-rc.4

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.
@@ -33,6 +33,16 @@ def _install():
33
33
  except OSError:
34
34
  pass
35
35
 
36
+ # Every other outbound call: the host and what it answered, never a byte of it. A search
37
+ # provider over its limit for a whole run was invisible until this row existed.
38
+ def dep(url, status, code=None):
39
+ try:
40
+ host = (urlsplit(str(url)).hostname or "").lower()
41
+ if not host or host in ("localhost", "127.0.0.1", "::1"):
42
+ return
43
+ write({"dep": {"at": int(time.time() * 1000), "host": host[:253], "status": int(status or 0), **({"code": str(code)[:40]} if code else {})}})
44
+ except Exception:
45
+ pass
36
46
  def is_model_call(url):
37
47
  try:
38
48
  parts = urlsplit(str(url))
@@ -330,6 +340,7 @@ def _install():
330
340
 
331
341
  def watch(request, response, sent):
332
342
  if not is_model_call(request.url):
343
+ dep(request.url, response.status_code)
333
344
  return response
334
345
  kind = response.headers.get("content-type", "")
335
346
  if getattr(response, "_content", None) is not None:
@@ -356,9 +367,11 @@ def _install():
356
367
  pass
357
368
  try:
358
369
  response = _send(self, request, *a, **k)
359
- except Exception:
370
+ except Exception as err:
360
371
  if is_model_call(request.url):
361
372
  meter(request.url, sent, 0, "", "")
373
+ else:
374
+ dep(request.url, 0, type(err).__name__)
362
375
  raise
363
376
  try:
364
377
  return watch(request, response, sent)
@@ -374,9 +387,11 @@ def _install():
374
387
  pass
375
388
  try:
376
389
  response = await _send(self, request, *a, **k)
377
- except Exception:
390
+ except Exception as err:
378
391
  if is_model_call(request.url):
379
392
  meter(request.url, sent, 0, "", "")
393
+ else:
394
+ dep(request.url, 0, type(err).__name__)
380
395
  raise
381
396
  try:
382
397
  return watch(request, response, sent)
@@ -392,8 +407,15 @@ def _install():
392
407
  note(request.url, request.body)
393
408
  except Exception:
394
409
  pass
395
- response = send(self, request, *a, **k)
396
410
  try:
411
+ response = send(self, request, *a, **k)
412
+ except Exception as err:
413
+ if not is_model_call(request.url):
414
+ dep(request.url, 0, type(err).__name__)
415
+ raise
416
+ try:
417
+ if not is_model_call(request.url):
418
+ dep(request.url, response.status_code)
397
419
  if is_model_call(request.url):
398
420
  # A streamed reply is counted as a call whose counts were not read.
399
421
  raw = response.content.decode("utf-8", "replace") if getattr(response, "_content_consumed", False) else ""
@@ -413,9 +435,16 @@ def _install():
413
435
  note(str_or_url, body)
414
436
  except Exception:
415
437
  pass
416
- response = await request(self, method, str_or_url, *a, **k)
438
+ try:
439
+ response = await request(self, method, str_or_url, *a, **k)
440
+ except Exception as err:
441
+ if not is_model_call(str_or_url):
442
+ dep(str_or_url, 0, type(err).__name__)
443
+ raise
417
444
  if is_model_call(str_or_url):
418
445
  response._cortad = (str_or_url, text(body))
446
+ else:
447
+ dep(str_or_url, response.status)
419
448
  return response
420
449
 
421
450
  # Metered when your app reads the reply, or, for one it streams, when the reply is let go.
package/lib/replay.mjs CHANGED
@@ -88,8 +88,9 @@ const meter = (() => {
88
88
  },
89
89
  // A service their settings name: only its setting, host and status travel, never a byte of it.
90
90
  dep(d) {
91
- if (!d || typeof d.env !== "string" || !/^[A-Z_][A-Z0-9_]*$/.test(d.env) || typeof d.host !== "string") return;
92
- deps.push({ at: count(d.at), env: d.env, host: d.host.slice(0, 253), status: count(d.status), ...(typeof d.code === "string" ? { code: d.code.slice(0, 40) } : {}) });
91
+ if (!d || typeof d.host !== "string" || !d.host) return;
92
+ const env = typeof d.env === "string" && /^[A-Z_][A-Z0-9_]*$/.test(d.env) ? d.env : undefined;
93
+ deps.push({ at: count(d.at), ...(env ? { env } : {}), host: d.host.slice(0, 253), status: count(d.status), ...(typeof d.code === "string" ? { code: d.code.slice(0, 40) } : {}) });
93
94
  if (deps.length > ROWS) deps.splice(0, deps.length - ROWS);
94
95
  },
95
96
  report() {
package/lib/trace.cjs CHANGED
@@ -196,7 +196,7 @@ if (FILE) {
196
196
  } catch { return null; }
197
197
  };
198
198
  const depRow = (dep, status, code) => {
199
- try { fs.appendFileSync(FILE, JSON.stringify({ dep: { at: Date.now(), env: dep.env, host: dep.host, status, code: code ? String(code).slice(0, 40) : undefined } }) + "\n", { mode: 0o600 }); } catch { /* the command is gone */ }
199
+ try { fs.appendFileSync(FILE, JSON.stringify({ dep: { at: Date.now(), ...(dep.env ? { env: dep.env } : {}), host: dep.host, status, code: code ? String(code).slice(0, 40) : undefined } }) + "\n", { mode: 0o600 }); } catch { /* the command is gone */ }
200
200
  };
201
201
 
202
202
  // Outbound: the model call your app makes while it handles that request.
@@ -210,7 +210,10 @@ if (FILE) {
210
210
  if (isModelCall(u.host, u.pathname)) url = u;
211
211
  } catch { /* not a URL we can read */ }
212
212
  if (!url) {
213
- const dep = depOf(input);
213
+ // Every outbound host, named by the setting that points at it when one does, and by the
214
+ // host alone otherwise: the search provider behind TAVILY_API_KEY has no URL setting.
215
+ let dep = depOf(input);
216
+ if (!dep) { try { const u = new URL(typeof input === "string" ? input : input && input.url ? input.url : String(input)); if (/^https?:$/.test(u.protocol) && !/^(localhost|127\.0\.0\.1|\[::1\])$/.test(u.hostname)) dep = { host: u.hostname }; } catch { /* not a URL */ } }
214
217
  if (!dep) return realFetch.apply(this, arguments);
215
218
  return realFetch.apply(this, arguments).then(
216
219
  (res) => { depRow(dep, res.status); return res; },
package/lib/verbs.mjs CHANGED
@@ -127,7 +127,13 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
127
127
  const more = await call("GET", path(page, d.runId ?? jobId));
128
128
  if (!more.ok) return more;
129
129
  d.findings = [...(d.findings ?? []), ...(more.data.findings ?? [])];
130
- if (more.data.byLine) d.byLine = [...(d.byLine ?? []), ...more.data.byLine];
130
+ // Every page carries the whole byLine; merged by line with the ids joined, or the first
131
+ // page's groups print twice.
132
+ for (const g of more.data.byLine ?? []) {
133
+ const held = (d.byLine ?? []).find((h) => h.path === g.path && h.line === g.line);
134
+ if (held) held.findingIds = [...new Set([...held.findingIds, ...g.findingIds])];
135
+ else d.byLine = [...(d.byLine ?? []), g];
136
+ }
131
137
  }
132
138
  return first;
133
139
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortad",
3
- "version": "0.3.0-rc.3",
3
+ "version": "0.3.0-rc.4",
4
4
  "description": "Connects the AI app on your machine to Cortad for test conversations, and gives your coding agent the MCP and skill to run them. No dependencies.",
5
5
  "bin": {
6
6
  "cortad": "local.mjs"