@synkro-sh/cli 1.6.86 → 1.6.88

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/dist/bootstrap.js CHANGED
@@ -2143,10 +2143,12 @@ export function appendSessionAction(sessionId: string, entry: SessionAction): vo
2143
2143
  appendFileSync(logPath, JSON.stringify(entry) + '\\n', 'utf-8');
2144
2144
  } catch {}
2145
2145
 
2146
- // Cloud: ship the action to the API transcript-sync (org from login JWT) with an
2147
- // explicit step so streamed single-action sends stay idempotent + ordered.
2146
+ // Cloud: stream the action to the session_actions hypertable (salience tagged
2147
+ // server-side, indexed) so the grade path pulls a BOUNDED indexed slice instead
2148
+ // of re-reading a multi-GB log \u2014 what lets us scale to million-tool-call
2149
+ // sessions. Fire-and-forget, idempotent by (session, step).
2148
2150
  if (deployIsCloud()) {
2149
- void syncTranscriptCloud(sessionId, '', [], [{ step, tool: entry.tool, summary: entry.summary, file: entry.file, outcome: entry.outcome }]);
2151
+ shipCloud(loadJwt(), '/api/v1/cli/session-action', { session_id: sessionId, step, tool: entry.tool, summary: entry.summary, file: entry.file, outcome: entry.outcome });
2150
2152
  return;
2151
2153
  }
2152
2154
  const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
@@ -2173,8 +2175,68 @@ export function readSessionLog(sessionId: string): SessionAction[] {
2173
2175
  } catch { return []; }
2174
2176
  }
2175
2177
 
2176
- export function compressSessionLog(actions: SessionAction[]): string {
2178
+ const RETRIEVAL_RECENT_N = 40;
2179
+ const PROCESS_KW = ['test', 'typecheck', 'tsc', 'lint', 'build', 'migrate', 'migration', 'deploy', 'publish', 'push', 'seed'];
2180
+
2181
+ // Is this session action security- or rule-relevant (i.e. can it participate in a
2182
+ // violation)? Conservative net \u2014 uses substring checks (no regex, to stay safe
2183
+ // inside the materialized-hook template). Plus rule-awareness: an action that
2184
+ // performs a process keyword an active rule actually mentions (e.g. a "pnpm test"
2185
+ // when a rule is about tests) is kept so precondition rules stay resolvable.
2186
+ function actionSalience(a: SessionAction, ruleText: string): string | null {
2187
+ const x = ((a.tool || '') + ' ' + (a.file || '') + ' ' + (a.summary || '')).toLowerCase();
2188
+ const has = (arr: string[]): boolean => { for (const k of arr) if (x.indexOf(k) >= 0) return true; return false; };
2189
+ if (has(['.env', 'credential', 'secret', 'token', '.pem', '.key', 'password', 'api key', 'api_key', 'api-key', 'authorization', 'bearer', 'oauth', '.aws'])) return 'secret';
2190
+ if (has(['curl ', 'wget ', 'ssh ', 'scp ', 'netcat', 'http://', 'https://', 'fetch('])) return 'network';
2191
+ if (has(['rm -rf', 'rm -r ', 'drop table', 'drop database', 'truncate', 'chmod', 'chown', 'sudo', 'kill ', 'wrangler', '--force', 'deploy', 'publish', 'git push'])) return 'destructive';
2192
+ for (const kw of PROCESS_KW) if (ruleText.indexOf(kw) >= 0 && x.indexOf(kw) >= 0) return 'rule';
2193
+ return null;
2194
+ }
2195
+
2196
+ // Cloud session log \u2014 retrieval style (validated 4/4 vs the last-200 dump in the
2197
+ // shadow corpus). Keeps EVERY security-salient + rule-relevant action in order
2198
+ // regardless of age (so cross-turn violations the dump's 200-window drops are
2199
+ // still seen), a recent verbatim window, and a COMPLETENESS-asserting summary of
2200
+ // the routine remainder (the assertion is what lets the grader trust the collapsed
2201
+ // part is benign \u2014 safe only because the salience net above is conservative).
2202
+ function compressSessionLogRetrieval(actions: SessionAction[], rules: Rule[]): string {
2203
+ if (actions.length === 0) return '';
2204
+ const ruleText = (rules || []).map(r => String((r as any).text || '').toLowerCase()).join(' ');
2205
+ const total = actions.length;
2206
+ const recentStart = Math.max(0, total - RETRIEVAL_RECENT_N);
2207
+ const salient: Array<{ a: SessionAction; s: string; i: number }> = [];
2208
+ const noise: Record<string, number> = {};
2209
+ for (let i = 0; i < recentStart; i++) {
2210
+ const a = actions[i];
2211
+ const s = actionSalience(a, ruleText);
2212
+ if (s) salient.push({ a, s, i });
2213
+ else noise[a.tool] = (noise[a.tool] || 0) + 1;
2214
+ }
2215
+ const lines: string[] = ['SESSION HISTORY (' + total + ' actions; security-salient + recent, in order):'];
2216
+ if (salient.length) {
2217
+ lines.push(' --- relevant (any point this session) ---');
2218
+ for (const r of salient) {
2219
+ const target = r.a.file || (r.a.summary || '').slice(0, 80);
2220
+ lines.push(' ' + (r.i + 1) + '. [' + r.s + '] ' + r.a.tool + ' ' + target + (r.a.outcome ? ' -> ' + r.a.outcome : ''));
2221
+ }
2222
+ }
2223
+ const noiseN = Object.values(noise).reduce((n, c) => n + c, 0);
2224
+ if (noiseN) {
2225
+ lines.push(' [' + noiseN + ' earlier routine actions (' + Object.entries(noise).map(([t, c]) => c + ' ' + t).join(', ') + '). The relevant list above is COMPLETE: every credential/secret read, outbound network call, and destructive or rule-gated action this session is listed there; these ' + noiseN + ' are non-salient in-repo reads/edits/builds only.]');
2226
+ }
2227
+ lines.push(' --- recent ---');
2228
+ for (let i = recentStart; i < total; i++) {
2229
+ const a = actions[i];
2230
+ lines.push(' ' + (i + 1) + '. ' + (a.summary || a.tool) + (a.outcome ? ' -> ' + a.outcome : ''));
2231
+ }
2232
+ return lines.join('\\n');
2233
+ }
2234
+
2235
+ export function compressSessionLog(actions: SessionAction[], rules?: Rule[]): string {
2177
2236
  if (actions.length === 0) return '';
2237
+ // Cloud grade path passes the active rules \u2192 retrieval-style context. Local
2238
+ // (no rules arg, or local storage mode) keeps the dump below, UNTOUCHED.
2239
+ if (rules !== undefined && !isLocalStorageMode()) return compressSessionLogRetrieval(actions, rules);
2178
2240
  const total = actions.length;
2179
2241
  const lines: string[] = [];
2180
2242
 
@@ -2214,6 +2276,39 @@ export function compressSessionLog(actions: SessionAction[]): string {
2214
2276
  return 'SESSION HISTORY (' + total + ' actions):\\n' + lines.join('\\n');
2215
2277
  }
2216
2278
 
2279
+ // Cloud: fetch the BOUNDED, indexed session context from Timescale (salient +
2280
+ // recent + complete summary) \u2014 flat cost at any session length. Returns null if
2281
+ // the session isn't streamed yet (brand-new / streaming lagging) so the caller
2282
+ // falls back to the local path.
2283
+ async function fetchSessionContext(sessionId: string): Promise<string | null> {
2284
+ const jwt = loadJwt();
2285
+ if (!jwt) return null;
2286
+ try {
2287
+ const r = await fetch(GATEWAY_URL + '/api/v1/hook/session-context', {
2288
+ method: 'POST',
2289
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
2290
+ body: JSON.stringify({ session_id: sessionId }),
2291
+ signal: AbortSignal.timeout(3000),
2292
+ });
2293
+ if (!r.ok) return null;
2294
+ const d = await r.json() as { context?: string };
2295
+ return (d && typeof d.context === 'string' && d.context) ? d.context : null;
2296
+ } catch {
2297
+ return null;
2298
+ }
2299
+ }
2300
+
2301
+ // Grade-time session log. CLOUD \u2192 Timescale-backed bounded context (this is the
2302
+ // million-tool-call path: no full-history read); falls back to the local
2303
+ // retrieval compressor only if the session isn't streamed yet. LOCAL \u2192 the
2304
+ // on-device dump, completely UNTOUCHED.
2305
+ export async function sessionLogForGrade(sessionId: string, rules: Rule[]): Promise<string> {
2306
+ if (isLocalStorageMode()) return compressSessionLog(readSessionLog(sessionId));
2307
+ const ctx = await fetchSessionContext(sessionId);
2308
+ if (ctx) return ctx;
2309
+ return compressSessionLog(readSessionLog(sessionId), rules);
2310
+ }
2311
+
2217
2312
  export function cleanupSessionLog(sessionId: string): void {
2218
2313
  const logPath = sessionLogPath(sessionId);
2219
2314
  if (!logPath) return;
@@ -3961,7 +4056,7 @@ import {
3961
4056
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
3962
4057
  parseVerdict, parseCombinedVerdict, combinedEditGradeEnabled, dispatchCapture, dispatchFinding, ruleMode, reconstructContent, isPathUnder, postWithRetry,
3963
4058
  readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
3964
- appendSessionAction, readSessionLog, compressSessionLog, log,
4059
+ appendSessionAction, readSessionLog, compressSessionLog, sessionLogForGrade, log,
3965
4060
  outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, hookSessionId, GATEWAY_URL,
3966
4061
  logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
3967
4062
  captureLineMetrics, cursorModelFromPayload, resolveTranscriptPath, isCursorInvokingCcHook,
@@ -4088,7 +4183,7 @@ async function main() {
4088
4183
  if (rt === 'local') {
4089
4184
  // \u2500\u2500\u2500 Local grading: org rules ONLY (channel 1, port 18929) \u2500\u2500\u2500
4090
4185
  const proposedShort = proposed.slice(0, 4000);
4091
- const sessionLog = compressSessionLog(readSessionLog(sessionId));
4186
+ const sessionLog = await sessionLogForGrade(sessionId, config.rules);
4092
4187
  const graderContent = 'file=' + filePath + ' content=' + proposedShort;
4093
4188
  const relevantRules = await filterRules(
4094
4189
  ruleFilterText(graderContent, transcript.userIntent || lastPrompt),
@@ -4281,7 +4376,7 @@ async function main() {
4281
4376
  recent_user_messages: transcript.recentUserMessages,
4282
4377
  recent_messages: transcript.recentMessages,
4283
4378
  recent_actions: transcript.recentActions,
4284
- session_history: compressSessionLog(readSessionLog(sessionId)),
4379
+ session_history: await sessionLogForGrade(sessionId, config.rules),
4285
4380
  session_id: sessionId || null,
4286
4381
  tool_use_id: toolUseId || null,
4287
4382
  cwd: cwd || null,
@@ -5352,7 +5447,7 @@ import process from 'node:process';
5352
5447
  import {
5353
5448
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
5354
5449
  parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
5355
- extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
5450
+ extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, sessionLogForGrade, log,
5356
5451
  outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isShellTool, hookSessionId, GATEWAY_URL,
5357
5452
  logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
5358
5453
  loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
@@ -5520,7 +5615,7 @@ async function main() {
5520
5615
  // mode was also checked up there.
5521
5616
 
5522
5617
  if (rt === 'local') {
5523
- const sessionLog = compressSessionLog(readSessionLog(sessionId));
5618
+ const sessionLog = await sessionLogForGrade(sessionId, config.rules);
5524
5619
  const relevantRules = await filterRules(
5525
5620
  ruleFilterText(command, transcript.userIntent || lastPrompt),
5526
5621
  config.rules,
@@ -5602,7 +5697,7 @@ async function main() {
5602
5697
  recent_user_messages: transcript.recentUserMessages,
5603
5698
  recent_messages: transcript.recentMessages,
5604
5699
  recent_actions: transcript.recentActions,
5605
- session_history: compressSessionLog(readSessionLog(sessionId)),
5700
+ session_history: await sessionLogForGrade(sessionId, config.rules),
5606
5701
  session_id: sessionId || null,
5607
5702
  tool_use_id: toolUseId || null,
5608
5703
  cwd: cwd || null,
@@ -5653,7 +5748,7 @@ main();
5653
5748
  import {
5654
5749
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
5655
5750
  parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
5656
- extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
5751
+ extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, sessionLogForGrade, log,
5657
5752
  outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isAgentTool, hookSessionId, GATEWAY_URL,
5658
5753
  logGraderUnavailable, graderUnavailableMessage, filterRules, normalizeMode, resolveTranscriptPath, isCursorInvokingCcHook,
5659
5754
  loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
@@ -5735,7 +5830,7 @@ async function main() {
5735
5830
  }
5736
5831
 
5737
5832
  if (rt === 'local') {
5738
- const sessionLog = compressSessionLog(readSessionLog(sessionId));
5833
+ const sessionLog = await sessionLogForGrade(sessionId, config.rules);
5739
5834
  const agentText = 'agent=' + subagentType + ' description=' + description + ' prompt=' + prompt.slice(0, 2000);
5740
5835
  const relevantRules = await filterRules(agentText, config.rules);
5741
5836
  const graderPrompt = [
@@ -5808,7 +5903,7 @@ async function main() {
5808
5903
  recent_user_messages: transcript.recentUserMessages,
5809
5904
  recent_messages: transcript.recentMessages,
5810
5905
  recent_actions: transcript.recentActions,
5811
- session_history: compressSessionLog(readSessionLog(sessionId)),
5906
+ session_history: await sessionLogForGrade(sessionId, config.rules),
5812
5907
  session_id: sessionId || null,
5813
5908
  tool_use_id: toolUseId || null,
5814
5909
  cwd: cwd || null,
@@ -5846,7 +5941,7 @@ main();
5846
5941
  PLAN_JUDGE_TS = `#!/usr/bin/env bun
5847
5942
  import {
5848
5943
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
5849
- parseVerdict, dispatchCapture, appendSessionAction, readSessionLog, compressSessionLog, postWithRetry, readStdin, log,
5944
+ parseVerdict, dispatchCapture, appendSessionAction, readSessionLog, compressSessionLog, sessionLogForGrade, postWithRetry, readStdin, log,
5850
5945
  outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isPlanTool, hookSessionId, GATEWAY_URL,
5851
5946
  filterRules, graderUnavailableMessage, logGraderUnavailable, resolveTranscriptPath, isCursorInvokingCcHook,
5852
5947
  loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
@@ -5953,7 +6048,7 @@ async function main() {
5953
6048
  }
5954
6049
 
5955
6050
  if (rt === 'local') {
5956
- const sessionLog = compressSessionLog(readSessionLog(sessionId));
6051
+ const sessionLog = await sessionLogForGrade(sessionId, config.rules);
5957
6052
  const relevantRules = await filterRules(plan.slice(0, 2000), config.rules);
5958
6053
  const graderPrompt = [
5959
6054
  'Working directory: ' + (cwd || '.'),
@@ -6386,7 +6481,7 @@ import {
6386
6481
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
6387
6482
  parseVerdict, dispatchCapture, dispatchFinding, ruleMode, normalizeMode, filterRules, ruleFilterText,
6388
6483
  isSafeInRepoRead, resolveTranscriptPath, postWithRetry, readStdin, hashCommand,
6389
- extractTranscript, readLastPrompt, readSessionLog, compressSessionLog,
6484
+ extractTranscript, readLastPrompt, readSessionLog, compressSessionLog, sessionLogForGrade,
6390
6485
  appendLocalTelemetry, logGraderUnavailable, graderUnavailableMessage, log, GATEWAY_URL,
6391
6486
  loadSynkroFile, effectiveGraderPool, synkroFilePresent,
6392
6487
  type Rule,
@@ -6559,7 +6654,7 @@ async function main() {
6559
6654
  }
6560
6655
 
6561
6656
  if (rt === 'local') {
6562
- const sessionLog = compressSessionLog(readSessionLog(sessionId));
6657
+ const sessionLog = await sessionLogForGrade(sessionId, config.rules);
6563
6658
  const relevantRules = await filterRules(
6564
6659
  ruleFilterText(command, transcript.userIntent || lastPrompt),
6565
6660
  config.rules,
@@ -10856,7 +10951,7 @@ function writeConfigEnv(opts) {
10856
10951
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
10857
10952
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
10858
10953
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
10859
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.86")}`
10954
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.88")}`
10860
10955
  ];
10861
10956
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
10862
10957
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -14899,7 +14994,7 @@ var args = process.argv.slice(2);
14899
14994
  var cmd = args[0] || "";
14900
14995
  var subArgs = args.slice(1);
14901
14996
  function printVersion() {
14902
- console.log("1.6.86");
14997
+ console.log("1.6.88");
14903
14998
  }
14904
14999
  function printHelp2() {
14905
15000
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents