mtech-ai-local-runner 0.1.64 → 0.1.65

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.
@@ -44,7 +44,8 @@ export function evaluateClaudeReview(input) {
44
44
  // Source content may reach the provider only inside the allowed read's tool result.
45
45
  const implicit = runs.reduce((n, r) => n + r.requests.filter(q => q.canaries.includes('source') && !q.toolResultIds.includes(IDS.allowed)).length, 0);
46
46
  const slashFirst = slash ? first(slash) : undefined;
47
- const delivery = !slash ? 'missing' : slashFirst?.firstUserText === CLAUDE_REVIEW_SLASH_PROMPT ? 'literal' : slash.terminal && !slashFirst ? 'refused_locally' : 'missing';
47
+ // Refused locally means no Messages request at all; any other request without the literal prompt is unexplained.
48
+ const delivery = !slash ? 'missing' : slashFirst?.firstUserText === CLAUDE_REVIEW_SLASH_PROMPT ? 'literal' : slash.terminal && messages(slash).length === 0 ? 'refused_locally' : 'missing';
48
49
  const versionLine = new RegExp(`^${input.release.version.replace(/\./g, '\\.')}(?:\\s|$)`);
49
50
  return [
50
51
  scenario('inventory', { copy_sha256_matches: input.copySha256 === input.release.binary_sha256, copy_unchanged: input.copyUnchanged,
@@ -118,6 +119,7 @@ export async function runClaudeReviewCampaign(input) {
118
119
  ...(input.authObservation ? { authObservation: { sha256: input.authObservation.sha256, decision_id: input.authObservation.observation.decision_id } } : {}),
119
120
  observations: { started_at: startedAt, finished_at: now().toISOString(), root: layout.root, runner_package_version: input.runnerVersion ?? packageVersion(),
120
121
  runs: runs.map(r => ({ kind: r.kind, model: r.model, duration_ms: r.durationMs, auxiliary_requests: r.requests.filter(q => q.kind === 'auxiliary').length,
121
- unknown_requests: r.requests.filter(q => q.kind === 'unknown' || q.kind === 'invalid').length, proxy_attempts: [...r.proxyAttempts], keychain_lookups: r.keychainOperations.length })) },
122
+ unknown_requests: r.requests.filter(q => q.kind === 'unknown' || q.kind === 'invalid').length, proxy_attempts: [...r.proxyAttempts], keychain_lookups: r.keychainOperations.length,
123
+ requests: r.requests.filter(q => q.kind === 'primary' || q.kind === 'auxiliary').map(q => ({ kind: q.kind, step: q.step, model_matches: q.model === r.model, prompt_matched: q.promptMatched, reminders: q.reminders, tools: q.toolNames.length })) })) },
122
124
  });
123
125
  }
@@ -12,10 +12,19 @@ function textOf(content) {
12
12
  const parts = content.filter((block) => record(block) && block.type === 'text' && typeof block.text === 'string').map(block => block.text);
13
13
  return parts.length ? parts.join('') : null;
14
14
  }
15
+ const REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
16
+ /** The user's own text. The real CLI prepends context in system-reminder tags, in its own block or the same one. */
17
+ function userText(content) {
18
+ const joined = textOf(content);
19
+ if (joined === null)
20
+ return { text: null, reminders: 0 };
21
+ const reminders = joined.match(REMINDER)?.length ?? 0;
22
+ return { text: reminders ? joined.replace(REMINDER, '').trim() : joined, reminders };
23
+ }
15
24
  const stopReason = (blocks) => blocks.some(block => block.type === 'tool_use') ? 'tool_use' : 'end_turn';
16
25
  /** Messages streaming events for fixed blocks: message_start, start/delta/stop per block, message_delta, message_stop. */
17
- export function claudeReviewSse(model, blocks) {
18
- const events = [['message_start', { type: 'message_start', message: { id: 'msg_review', type: 'message', role: 'assistant', model, content: [],
26
+ export function claudeReviewSse(id, model, blocks) {
27
+ const events = [['message_start', { type: 'message_start', message: { id, type: 'message', role: 'assistant', model, content: [],
19
28
  stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } } }]];
20
29
  blocks.forEach((block, index) => {
21
30
  events.push(['content_block_start', { type: 'content_block_start', index, content_block: block.type === 'text' ? { type: 'text', text: '' } : { type: 'tool_use', id: block.id, name: block.name, input: {} } }], ['content_block_delta', { type: 'content_block_delta', index, delta: block.type === 'text' ? { type: 'text_delta', text: block.text } : { type: 'input_json_delta', partial_json: JSON.stringify(block.input) } }], ['content_block_stop', { type: 'content_block_stop', index }]);
@@ -33,7 +42,7 @@ async function listen(server) {
33
42
  export async function startClaudeReviewProvider(script) {
34
43
  const requests = [], toolResults = new Map(), proxyAttempts = [];
35
44
  const canaryHits = Object.fromEntries(Object.keys(script.canaries).map(name => [name, 0]));
36
- let fakeKeyInBody = 0, origin = '';
45
+ let fakeKeyInBody = 0, origin = '', responses = 0;
37
46
  const keep = (request) => { if (requests.length < MAX_REQUESTS)
38
47
  requests.push(request); };
39
48
  const attempt = (target) => { if (proxyAttempts.length < MAX_ATTEMPTS)
@@ -75,7 +84,7 @@ export async function startClaudeReviewProvider(script) {
75
84
  if (raw.includes(script.fakeKey))
76
85
  fakeKeyInBody++;
77
86
  const apiKey = req.headers['x-api-key'];
78
- const base = { step: null, model: null, toolNames: [], toolResultIds: [], apiKey: typeof apiKey === 'string' ? apiKey : null, authorization: req.headers.authorization !== undefined, firstUserText: null, canaries: hit };
87
+ const base = { step: null, model: null, toolNames: [], toolResultIds: [], apiKey: typeof apiKey === 'string' ? apiKey : null, authorization: req.headers.authorization !== undefined, firstUserText: null, canaries: hit, promptMatched: false, reminders: 0 };
79
88
  if (url.pathname.startsWith('/planted-mcp/')) {
80
89
  keep({ ...base, kind: 'planted_mcp' });
81
90
  res.writeHead(404);
@@ -113,22 +122,23 @@ export async function startClaudeReviewProvider(script) {
113
122
  if (!toolResults.has(block.tool_use_id))
114
123
  toolResults.set(block.tool_use_id, { isError: block.is_error === true, text: typeof block.content === 'string' ? block.content : JSON.stringify(block.content ?? '') });
115
124
  }
116
- const firstUser = messages.find(message => message.role === 'user'), firstUserText = firstUser ? textOf(firstUser.content) : null;
125
+ const firstUser = messages.find(message => message.role === 'user'), { text: firstUserText, reminders } = firstUser ? userText(firstUser.content) : { text: null, reminders: 0 };
117
126
  // The scripted turn: the launched model, the exact review prompt as the first user text and an offered tool list.
118
127
  // Title and other auxiliary requests never consume a step; the step is the number of assistant turns so far.
119
- const primary = model === script.model && firstUserText === script.prompt && toolNames.length > 0;
128
+ const promptMatched = firstUserText === script.prompt, primary = model === script.model && promptMatched && toolNames.length > 0;
120
129
  const step = primary ? messages.filter(message => message.role === 'assistant').length : null;
121
- keep({ ...base, kind: primary ? 'primary' : 'auxiliary', step, model, toolNames, toolResultIds, firstUserText: firstUserText === null ? null : firstUserText.slice(0, 256) });
130
+ keep({ ...base, kind: primary ? 'primary' : 'auxiliary', step, model, toolNames, toolResultIds, firstUserText: firstUserText === null ? null : firstUserText.slice(0, 256), promptMatched, reminders });
122
131
  const next = step === null ? undefined : script.steps[step];
123
132
  const blocks = !primary ? [{ type: 'text', text: CLAUDE_REVIEW_AUXILIARY_TEXT }] : next ? [{ type: 'tool_use', ...next.tool }] : [{ type: 'text', text: CLAUDE_REVIEW_FINAL_TEXT }];
124
- const answering = model ?? script.model;
133
+ // Each response has its own id, as from the API: the CLI (2.1.308) treats assistant messages that share an id as one response.
134
+ const answering = model ?? script.model, id = `msg_review_${++responses}`;
125
135
  if (body.stream === true) {
126
136
  res.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' });
127
- res.end(claudeReviewSse(answering, blocks));
137
+ res.end(claudeReviewSse(id, answering, blocks));
128
138
  return;
129
139
  }
130
140
  res.writeHead(200, { 'content-type': 'application/json' });
131
- res.end(JSON.stringify({ id: 'msg_review', type: 'message', role: 'assistant', model: answering, content: blocks, stop_reason: stopReason(blocks), stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } }));
141
+ res.end(JSON.stringify({ id, type: 'message', role: 'assistant', model: answering, content: blocks, stop_reason: stopReason(blocks), stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 } }));
132
142
  }
133
143
  const port = await listen(server);
134
144
  origin = `http://127.0.0.1:${port}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.64",
3
+ "version": "0.1.65",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "bin": {