@clear-capabilities/agentic-security-scanner 0.172.0 → 0.172.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.
- package/CHANGELOG.md +14 -0
- package/dist/1336.index.js +101 -101
- package/dist/7405.index.js +23 -6
- package/dist/9382.index.js +205 -39
- package/dist/agentic-security.mjs +3 -3
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +4 -4
- package/src/context-intelligence/CLAUDE.md +4 -0
- package/src/context-intelligence/adapters/haskell-hie.js +28 -1
- package/src/context-intelligence/canonical.js +25 -0
- package/src/context-intelligence/cli.js +14 -10
- package/src/context-intelligence/config.js +2 -1
- package/src/context-intelligence/controls.js +59 -9
- package/src/context-intelligence/entrypoints.js +2 -2
- package/src/context-intelligence/evaluation/index.js +11 -4
- package/src/context-intelligence/evaluation/scripted-reader.js +12 -2
- package/src/context-intelligence/reachability.js +8 -2
- package/src/context-intelligence/review.js +61 -12
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,20 @@
|
|
|
9
9
|
> make the history less accurate, not more.
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
## 0.172.1 - Deterministic Context Intelligence: adversarial review fixes
|
|
13
|
+
|
|
14
|
+
An adversarial review of the first release found nine problems, each demonstrated against the shipped code and each now pinned by a regression test (`scanner/test/context-intelligence/premortem.test.js`).
|
|
15
|
+
|
|
16
|
+
- **False ownership protection.** A route that built SQL from `req.params.id` and merely mentioned `req.user.id` and `owner_name` in the same call reported `ownership: present` with a `complete` bundle. An owner field now counts only when it is tied to the principal in the same clause, and the principal's own name is never read as an owner field. `present` carries a `strength` (`observed` or `inferred`).
|
|
17
|
+
- **Discarded guards.** `isOwner(...)` called as a bare statement no longer counts as a guard; it is `evaluated_and_ineffective`.
|
|
18
|
+
- **Prompt injection through the route label.** The label sat in the instruction text outside the untrusted fence. It is now sanitised and placed inside the fence.
|
|
19
|
+
- **Remote egress.** A non-loopback endpoint now needs `--allow-remote` or `review.allowRemote`; the egress policy and redaction still apply after approval.
|
|
20
|
+
- **Terminal escapes.** Every line the command prints has control characters and bidirectional marks replaced.
|
|
21
|
+
- **Citations.** A citation must now be about its cited lines (not only inside the bundle), spans at most 60 lines, and `no_issue_observed` must cite a control the deterministic pass also found.
|
|
22
|
+
- **Unmeasured numbers.** Precision, recall, F1 and calls per true positive are withheld while the interval is unmeasured; the CSV carries a `synthetic` column.
|
|
23
|
+
- **Compiler export.** A reference must name a callee that its cited line contains, or the whole import degrades.
|
|
24
|
+
- **Locale.** The one locale-sensitive ordering now uses code-unit order, so manifests do not differ by host locale.
|
|
25
|
+
|
|
12
26
|
## 0.172.0 - Deterministic Context Intelligence (`context` command)
|
|
13
27
|
|
|
14
28
|
An opt-in, additive subsystem. The default scan, `hunt` and every existing command are unchanged.
|
package/dist/1336.index.js
CHANGED
|
@@ -9069,107 +9069,107 @@ function dbg(...args) {
|
|
|
9069
9069
|
// === Body ===
|
|
9070
9070
|
|
|
9071
9071
|
var ASM_CONSTS = {
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9072
|
+
21603560: () => { return current_async_call_id; },
|
|
9073
|
+
21603594: ($0) => { reject_async($0, new Error("Memory allocation failed")); },
|
|
9074
|
+
21603655: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9075
|
+
21603710: () => { return current_async_call_id; },
|
|
9076
|
+
21603744: ($0) => { reject_async($0, new Error("Memory allocation failed")); },
|
|
9077
|
+
21603805: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9078
|
+
21603860: () => { return current_async_call_id; },
|
|
9079
|
+
21603894: ($0) => { reject_async($0, new Error("Memory allocation failed")); },
|
|
9080
|
+
21603955: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9081
|
+
21604010: () => { return current_async_call_id; },
|
|
9082
|
+
21604044: ($0) => { reject_async($0, new Error("Memory allocation failed")); },
|
|
9083
|
+
21604105: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9084
|
+
21604160: () => { return current_async_call_id; },
|
|
9085
|
+
21604194: ($0) => { reject_async($0, new Error("Memory allocation failed")); },
|
|
9086
|
+
21604255: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9087
|
+
21604310: ($0, $1) => { resolve_async($1, $0); },
|
|
9088
|
+
21604337: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9089
|
+
21604403: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9090
|
+
21604454: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9091
|
+
21604496: ($0, $1) => { resolve_async($1, $0); },
|
|
9092
|
+
21604523: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9093
|
+
21604589: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9094
|
+
21604640: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9095
|
+
21604682: ($0, $1) => { resolve_async($1, $0); },
|
|
9096
|
+
21604709: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9097
|
+
21604775: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9098
|
+
21604826: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9099
|
+
21604868: ($0, $1) => { resolve_async($1, $0); },
|
|
9100
|
+
21604895: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9101
|
+
21604961: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9102
|
+
21605012: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9103
|
+
21605054: ($0, $1) => { resolve_async($1, $0); },
|
|
9104
|
+
21605081: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9105
|
+
21605147: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9106
|
+
21605198: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9107
|
+
21605240: () => { return current_async_call_id; },
|
|
9108
|
+
21605274: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9109
|
+
21605329: ($0, $1) => { resolve_async($1, UTF8ToString($0)); },
|
|
9110
|
+
21605370: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9111
|
+
21605436: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9112
|
+
21605487: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9113
|
+
21605529: () => { return current_async_call_id; },
|
|
9114
|
+
21605563: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9115
|
+
21605618: ($0, $1) => { resolve_async($1, $0); },
|
|
9116
|
+
21605645: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9117
|
+
21605711: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9118
|
+
21605762: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9119
|
+
21605804: () => { return current_async_call_id; },
|
|
9120
|
+
21605838: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9121
|
+
21605893: ($0, $1) => { resolve_async($1, $0); },
|
|
9122
|
+
21605920: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9123
|
+
21605986: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9124
|
+
21606037: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9125
|
+
21606079: () => { return current_async_call_id; },
|
|
9126
|
+
21606113: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9127
|
+
21606168: ($0, $1) => { resolve_async($1, $0); },
|
|
9128
|
+
21606195: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9129
|
+
21606261: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9130
|
+
21606312: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9131
|
+
21606354: () => { return current_async_call_id; },
|
|
9132
|
+
21606388: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9133
|
+
21606443: ($0, $1) => { resolve_async($1, $0); },
|
|
9134
|
+
21606470: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9135
|
+
21606536: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9136
|
+
21606587: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9137
|
+
21606629: () => { return current_async_call_id; },
|
|
9138
|
+
21606663: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9139
|
+
21606718: ($0, $1) => { resolve_async($1, $0); },
|
|
9140
|
+
21606745: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9141
|
+
21606811: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9142
|
+
21606862: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9143
|
+
21606904: () => { return current_async_call_id; },
|
|
9144
|
+
21606938: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9145
|
+
21606993: ($0, $1) => { resolve_async($1, $0); },
|
|
9146
|
+
21607020: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9147
|
+
21607086: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9148
|
+
21607137: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9149
|
+
21607179: () => { return current_async_call_id; },
|
|
9150
|
+
21607213: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9151
|
+
21607268: ($0, $1) => { resolve_async($1, $0); },
|
|
9152
|
+
21607295: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9153
|
+
21607361: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9154
|
+
21607412: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9155
|
+
21607454: () => { return current_async_call_id; },
|
|
9156
|
+
21607488: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9157
|
+
21607543: ($0, $1) => { resolve_async($1, $0); },
|
|
9158
|
+
21607570: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9159
|
+
21607636: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9160
|
+
21607687: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9161
|
+
21607729: () => { return current_async_call_id; },
|
|
9162
|
+
21607763: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9163
|
+
21607818: ($0, $1) => { resolve_async($1, $0); },
|
|
9164
|
+
21607845: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9165
|
+
21607911: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9166
|
+
21607962: () => { clearTimeout(threadTimeouts.shift()); },
|
|
9167
|
+
21608004: () => { return current_async_call_id; },
|
|
9168
|
+
21608038: () => { threadTimeouts.push(setTimeout(() => {}, 600000)); },
|
|
9169
|
+
21608093: ($0, $1) => { resolve_async($1, $0); },
|
|
9170
|
+
21608120: ($0) => { reject_async($0, new Error('failed with unknown exception')); },
|
|
9171
|
+
21608186: ($0, $1) => { reject_async($1, new Error(UTF8ToString($0))); },
|
|
9172
|
+
21608237: () => { clearTimeout(threadTimeouts.shift()); }
|
|
9173
9173
|
};
|
|
9174
9174
|
|
|
9175
9175
|
// end include: preamble.js
|
package/dist/7405.index.js
CHANGED
|
@@ -114,6 +114,8 @@ function metricsFor(units, protocolHash, name) {
|
|
|
114
114
|
precision: (0,_posture_evaluation_interval_js__WEBPACK_IMPORTED_MODULE_5__/* .precisionOf */ .ij)(c), recall: (0,_posture_evaluation_interval_js__WEBPACK_IMPORTED_MODULE_5__/* .recallOf */ .HR)(c), f1: (0,_posture_evaluation_interval_js__WEBPACK_IMPORTED_MODULE_5__/* .f1Of */ .li)(c),
|
|
115
115
|
precisionInterval: pn > 0 ? (0,_posture_evaluation_interval_js__WEBPACK_IMPORTED_MODULE_5__/* .wilsonInterval */ .YV)(c.tp, pn) : { status: 'unmeasured', reason: 'nothing was reported' },
|
|
116
116
|
f1Interval: f1,
|
|
117
|
+
// A point estimate with no interval around it reads as a finding. It is reportable only when the interval is measured.
|
|
118
|
+
reportable: f1 && f1.status === 'measured',
|
|
117
119
|
};
|
|
118
120
|
}
|
|
119
121
|
|
|
@@ -149,6 +151,9 @@ function readLedger(root) {
|
|
|
149
151
|
const pct = (x) => (x === null || x === undefined ? 'n/a' : (x * 100).toFixed(0) + '%');
|
|
150
152
|
const interval = (i) => (!i || i.status !== 'measured' ? `unmeasured (${i && i.reason ? i.reason.split(',')[0] : 'no data'})` : `${pct(i.low)} to ${pct(i.high)}`);
|
|
151
153
|
|
|
154
|
+
// Precision, recall and F1 are printed only when their interval is measured; otherwise the counts stand alone.
|
|
155
|
+
const shown = (m, x) => (m.reportable ? pct(x) : 'withheld');
|
|
156
|
+
|
|
152
157
|
function renderMarkdown(card) {
|
|
153
158
|
const L = [];
|
|
154
159
|
L.push('# Context Intelligence evaluation scorecard', '');
|
|
@@ -163,18 +168,20 @@ function renderMarkdown(card) {
|
|
|
163
168
|
for (const s of card.systems) {
|
|
164
169
|
if (s.status !== 'measured') { L.push(`| ${s.system} | ${s.status}: ${s.reason} | | | | | | | | | | |`); continue; }
|
|
165
170
|
const m = s.metrics;
|
|
166
|
-
L.push(`| ${s.system} | measured | ${m.counts.tp} | ${m.counts.fp} | ${m.counts.fn} | ${m.counts.tn} | ${
|
|
171
|
+
L.push(`| ${s.system} | measured | ${m.counts.tp} | ${m.counts.fp} | ${m.counts.fn} | ${m.counts.tn} | ${shown(m, m.precision)} | ${shown(m, m.recall)} | ${shown(m, m.f1)} | ${interval(m.f1Interval)} | ${s.calls} | ${s.promptChars} |`);
|
|
167
172
|
}
|
|
168
173
|
L.push('', '## Economics (estimated; no price supplied)', '', '| system | median ctx tokens | P95 | P99 | model calls | calls per true positive | wall ms |', '|---|---|---|---|---|---|---|');
|
|
169
|
-
for (const s of card.systems) if (s.economics) { const e = s.economics; L.push(`| ${s.system} | ${e.contextTokens.median ?? 'n/a'} | ${e.contextTokens.p95 ?? 'n/a'} | ${e.contextTokens.p99 ?? 'n/a'} | ${e.modelCalls} | ${e.callsPerTruePositive === null ? '
|
|
174
|
+
for (const s of card.systems) if (s.economics) { const e = s.economics; L.push(`| ${s.system} | ${e.contextTokens.median ?? 'n/a'} | ${e.contextTokens.p95 ?? 'n/a'} | ${e.contextTokens.p99 ?? 'n/a'} | ${e.modelCalls} | ${e.callsPerTruePositive === null || !s.metrics || !s.metrics.reportable ? 'withheld' : e.callsPerTruePositive.toFixed(1)} | ${e.wallMs} |`); }
|
|
175
|
+
L.push('', 'Precision, recall, F1 and calls per true positive are withheld while their interval is unmeasured (fewer than 10 independent groups). The counts above are what was observed; they are not an estimate of anything.');
|
|
170
176
|
L.push('', `Replay: \`${card.replay}\``, '');
|
|
171
177
|
if (card.leakage.length) { L.push('## Leakage hits', ...card.leakage.map(h => `- ${h.where} ${h.file || ''} ${h.term}`), ''); }
|
|
172
178
|
return L.join('\n');
|
|
173
179
|
}
|
|
174
180
|
|
|
175
181
|
function renderCsv(card) {
|
|
176
|
-
|
|
177
|
-
|
|
182
|
+
// The marker is a column, not a banner: a CSV is the format that gets pasted into a spreadsheet without its header text.
|
|
183
|
+
const rows = [['system', 'case', 'variant', 'vulnerable', 'flagged', 'outcome', 'calls', 'promptChars', 'synthetic']];
|
|
184
|
+
for (const s of card.systems) for (const u of (s.units || [])) rows.push([s.system, u.caseId, u.variant, u.vulnerable, u.flagged, u.outcome, u.calls, u.promptChars, card.synthetic === false ? false : true]);
|
|
178
185
|
return rows.map(r => r.join(',')).join('\n') + '\n';
|
|
179
186
|
}
|
|
180
187
|
|
|
@@ -625,6 +632,16 @@ function stripUntrustedKeepUser(t) { return stripUntrusted(t).replace(/""/g, '""
|
|
|
625
632
|
|
|
626
633
|
function cite(l, role, claim) { return { role, file: l.file, startLine: l.n, endLine: l.n, claim }; }
|
|
627
634
|
|
|
635
|
+
/** A reader that cites a line says what is on it: the claim quotes the cited text, as a careful model would. */
|
|
636
|
+
function quoteCited(obj, blocks) {
|
|
637
|
+
const text = (c) => {
|
|
638
|
+
for (const b of blocks) if (b.file === c.file) { const l = b.lines.find(x => x.n === c.startLine); if (l) return l.text.trim().slice(0, 100); }
|
|
639
|
+
return '';
|
|
640
|
+
};
|
|
641
|
+
const fix = (list) => (Array.isArray(list) ? list.map(c => ({ ...c, claim: `${c.claim}: ${text(c)}` })) : list);
|
|
642
|
+
return { ...obj, evidence: fix(obj.evidence), observedMitigations: fix(obj.observedMitigations) };
|
|
643
|
+
}
|
|
644
|
+
|
|
628
645
|
/** The decision for a pack, from the observations. Returns the review JSON object. */
|
|
629
646
|
function decide(packId, obs, ctx) {
|
|
630
647
|
const base = { observedMitigations: [], attackerControl: [], uncertainties: [], likelihood: 'low' };
|
|
@@ -673,7 +690,7 @@ function scriptedReader() {
|
|
|
673
690
|
const invoke = async (prompt) => {
|
|
674
691
|
if (/^QUESTION PACK: /m.test(prompt)) {
|
|
675
692
|
const packId = /^QUESTION PACK: ([\w-]+)\//m.exec(prompt)[1];
|
|
676
|
-
const label = /^ENTRY POINT
|
|
693
|
+
const label = (/^ENTRY POINT LABEL \(data\): (.*)$/m.exec(prompt) || [])[1] || '';
|
|
677
694
|
const { blocks, unresolved, notIncluded, limit } = blocksFromReviewPrompt(prompt);
|
|
678
695
|
const routes = findRoutes(blocks);
|
|
679
696
|
const [method, ...rest] = label.split(' ');
|
|
@@ -682,7 +699,7 @@ function scriptedReader() {
|
|
|
682
699
|
if (!route) return JSON.stringify({ evidence: [], observedMitigations: [], conclusion: 'insufficient_evidence', invariant: 'unspecified', uncertainties: ['the registration is not in the provided context'] });
|
|
683
700
|
const obs = analyseRoute(route, blocks);
|
|
684
701
|
const complete = !unresolved && !notIncluded && !limit;
|
|
685
|
-
return JSON.stringify(decide(packId, obs, { complete, unresolved }));
|
|
702
|
+
return JSON.stringify(quoteCited(decide(packId, obs, { complete, unresolved }), blocks));
|
|
686
703
|
}
|
|
687
704
|
// hunt prompt: report the candidates for the lens, with a location
|
|
688
705
|
const lens = /Your lens is ([\w ]+)\./.exec(prompt);
|