sneakoscope 0.6.79 → 0.6.80

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "0.6.79",
4
+ "version": "0.6.80",
5
5
  "description": "Sneakoscope Codex: database-safe Codex CLI/App harness with Team, Goal, AutoResearch, TriWiki, and Honest Mode.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
package/src/cli/main.mjs CHANGED
@@ -440,10 +440,11 @@ async function wizard(args = []) {
440
440
  const rl = readline.createInterface({ input, output });
441
441
  try {
442
442
  console.log('ㅅㅋㅅ Setup UI\n');
443
- console.log(`Current package: ${PACKAGE_VERSION}`);
443
+ const currentPackage = await effectivePackageVersion();
444
+ console.log(`Current package: ${currentPackage}`);
444
445
  const latest = await npmPackageVersion('sneakoscope');
445
446
  if (latest.version) {
446
- const needsUpdate = compareVersions(latest.version, PACKAGE_VERSION) > 0;
447
+ const needsUpdate = compareVersions(latest.version, currentPackage) > 0;
447
448
  console.log(`Latest on npm: ${latest.version}${needsUpdate ? ' (update available)' : ''}`);
448
449
  if (needsUpdate) {
449
450
  const update = await askChoice(rl, 'Update SKS before setup?', ['yes', 'no'], 'yes');
@@ -496,11 +497,13 @@ async function askChoice(rl, question, choices, fallback) {
496
497
 
497
498
  async function updateCheck(args = []) {
498
499
  const latest = await npmPackageVersion('sneakoscope');
500
+ const currentPackage = await effectivePackageVersion();
499
501
  const result = {
500
502
  package: 'sneakoscope',
501
- current: PACKAGE_VERSION,
503
+ current: currentPackage,
504
+ runtime_current: PACKAGE_VERSION,
502
505
  latest: latest.version,
503
- update_available: latest.version ? compareVersions(latest.version, PACKAGE_VERSION) > 0 : false,
506
+ update_available: latest.version ? compareVersions(latest.version, currentPackage) > 0 : false,
504
507
  error: latest.error || null
505
508
  };
506
509
  if (flag(args, '--json')) return console.log(JSON.stringify(result, null, 2));
@@ -1124,14 +1127,15 @@ async function madHighCommand(args = []) {
1124
1127
  async function maybePromptSksUpdateForMad(args = []) {
1125
1128
  if (flag(args, '--json') || flag(args, '--skip-update-check') || process.env.SKS_SKIP_UPDATE_CHECK === '1') return { status: 'skipped' };
1126
1129
  const latest = await npmPackageVersion('sneakoscope');
1127
- if (!latest.version || compareVersions(latest.version, PACKAGE_VERSION) <= 0) return { status: 'current', latest: latest.version || null, error: latest.error || null };
1130
+ const currentPackage = await effectivePackageVersion();
1131
+ if (!latest.version || compareVersions(latest.version, currentPackage) <= 0) return { status: 'current', latest: latest.version || null, error: latest.error || null };
1128
1132
  const command = 'npm i -g sneakoscope@latest';
1129
1133
  if (flag(args, '--yes') || flag(args, '-y')) return installSksLatest(command, latest.version);
1130
1134
  if (!canAskYesNo()) {
1131
- console.log(`SKS update available: ${PACKAGE_VERSION} -> ${latest.version}. Run: ${command}`);
1135
+ console.log(`SKS update available: ${currentPackage} -> ${latest.version}. Run: ${command}`);
1132
1136
  return { status: 'available', latest: latest.version, command };
1133
1137
  }
1134
- const answer = (await askPostinstallQuestion(`SKS ${PACKAGE_VERSION} -> ${latest.version} update before MAD launch? [Y/n] `)).trim();
1138
+ const answer = (await askPostinstallQuestion(`SKS ${currentPackage} -> ${latest.version} update before MAD launch? [Y/n] `)).trim();
1135
1139
  const yes = answer === '' || /^(y|yes|예|네|응)$/i.test(answer);
1136
1140
  if (!yes) return { status: 'skipped_by_user', latest: latest.version, command };
1137
1141
  return installSksLatest(command, latest.version);
@@ -1899,6 +1903,15 @@ async function npmPackageVersion(name) {
1899
1903
  return { version: result.stdout.trim().split(/\s+/).pop() };
1900
1904
  }
1901
1905
 
1906
+ async function effectivePackageVersion() {
1907
+ const pkg = await readJson(path.join(packageRoot(), 'package.json'), {}).catch(() => ({}));
1908
+ return highestVersion([PACKAGE_VERSION, pkg.version]);
1909
+ }
1910
+
1911
+ function highestVersion(versions = []) {
1912
+ return versions.filter(Boolean).reduce((best, candidate) => compareVersions(candidate, best) > 0 ? candidate : best, '0.0.0');
1913
+ }
1914
+
1902
1915
  function compareVersions(a, b) {
1903
1916
  const pa = String(a || '').split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
1904
1917
  const pb = String(b || '').split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
@@ -2263,6 +2276,81 @@ async function selftest() {
2263
2276
  const hookState = await readJson(stateFile(hookGoalTmp), {});
2264
2277
  if (hookState.phase !== 'GOAL_READY' || hookState.mode !== 'GOAL') throw new Error('selftest failed: $Goal hook did not set ready state');
2265
2278
  if (!(await exists(path.join(missionDir(hookGoalTmp, hookState.mission_id), GOAL_WORKFLOW_ARTIFACT)))) throw new Error('selftest failed: $Goal hook did not write goal workflow artifact');
2279
+ const hookUpdateCurrentTmp = tmpdir();
2280
+ await initProject(hookUpdateCurrentTmp, {});
2281
+ const hookUpdateCurrentPayload = JSON.stringify({ cwd: hookUpdateCurrentTmp, prompt: '상태 확인해줘' });
2282
+ const hookUpdateCurrentResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
2283
+ cwd: hookUpdateCurrentTmp,
2284
+ input: hookUpdateCurrentPayload,
2285
+ env: { SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '9.9.9' },
2286
+ timeoutMs: 15000,
2287
+ maxOutputBytes: 256 * 1024
2288
+ });
2289
+ if (hookUpdateCurrentResult.code !== 0) throw new Error(`selftest failed: current update hook exited ${hookUpdateCurrentResult.code}: ${hookUpdateCurrentResult.stderr}`);
2290
+ const hookUpdateCurrentJson = JSON.parse(hookUpdateCurrentResult.stdout);
2291
+ const hookUpdateCurrentContext = hookUpdateCurrentJson.hookSpecificOutput?.additionalContext || '';
2292
+ if (String(hookUpdateCurrentContext).includes('Update SKS now') || String(hookUpdateCurrentContext).includes('Skip update for this conversation')) throw new Error('selftest failed: hook prompted for update even though installed SKS is current');
2293
+ const hookUpdateCurrentState = await readJson(path.join(hookUpdateCurrentTmp, '.sneakoscope', 'state', 'update-check.json'), {});
2294
+ if (hookUpdateCurrentState.pending_offer) throw new Error('selftest failed: current installed SKS left a pending update offer');
2295
+ if (hookUpdateCurrentState.current !== '9.9.9' || hookUpdateCurrentState.runtime_current !== PACKAGE_VERSION || hookUpdateCurrentState.installed_current !== '9.9.9') throw new Error('selftest failed: hook did not record effective installed SKS version');
2296
+ const hookUpdatePendingTmp = tmpdir();
2297
+ await initProject(hookUpdatePendingTmp, {});
2298
+ await writeJsonAtomic(path.join(hookUpdatePendingTmp, '.sneakoscope', 'state', 'update-check.json'), {
2299
+ current: PACKAGE_VERSION,
2300
+ latest: '9.9.9',
2301
+ pending_offer: { conversation_id: hookUpdatePendingTmp, latest: '9.9.9', offered_at: nowIso() }
2302
+ });
2303
+ const hookUpdatePendingPayload = JSON.stringify({ cwd: hookUpdatePendingTmp, prompt: 'Update SKS now' });
2304
+ const hookUpdatePendingResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
2305
+ cwd: hookUpdatePendingTmp,
2306
+ input: hookUpdatePendingPayload,
2307
+ env: { SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '9.9.9' },
2308
+ timeoutMs: 15000,
2309
+ maxOutputBytes: 256 * 1024
2310
+ });
2311
+ if (hookUpdatePendingResult.code !== 0) throw new Error(`selftest failed: stale pending update hook exited ${hookUpdatePendingResult.code}: ${hookUpdatePendingResult.stderr}`);
2312
+ const hookUpdatePendingJson = JSON.parse(hookUpdatePendingResult.stdout);
2313
+ const hookUpdatePendingContext = hookUpdatePendingJson.hookSpecificOutput?.additionalContext || '';
2314
+ if (String(hookUpdatePendingContext).includes('user accepted update') || String(hookUpdatePendingContext).includes('Before doing other work')) throw new Error('selftest failed: current installed SKS accepted a stale pending update offer');
2315
+ const hookUpdatePendingState = await readJson(path.join(hookUpdatePendingTmp, '.sneakoscope', 'state', 'update-check.json'), {});
2316
+ if (hookUpdatePendingState.pending_offer) throw new Error('selftest failed: stale pending update offer was not cleared after installed SKS became current');
2317
+ const hookUpdateSkippedTmp = tmpdir();
2318
+ await initProject(hookUpdateSkippedTmp, {});
2319
+ await writeJsonAtomic(path.join(hookUpdateSkippedTmp, '.sneakoscope', 'state', 'update-check.json'), {
2320
+ current: PACKAGE_VERSION,
2321
+ latest: '9.9.9',
2322
+ skipped: { conversation_id: hookUpdateSkippedTmp, latest: '9.9.9', skipped_at: nowIso() }
2323
+ });
2324
+ const hookUpdateSkippedPayload = JSON.stringify({ cwd: hookUpdateSkippedTmp, prompt: '상태 확인해줘' });
2325
+ const hookUpdateSkippedResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
2326
+ cwd: hookUpdateSkippedTmp,
2327
+ input: hookUpdateSkippedPayload,
2328
+ env: { SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '9.9.9' },
2329
+ timeoutMs: 15000,
2330
+ maxOutputBytes: 256 * 1024
2331
+ });
2332
+ if (hookUpdateSkippedResult.code !== 0) throw new Error(`selftest failed: stale skipped update hook exited ${hookUpdateSkippedResult.code}: ${hookUpdateSkippedResult.stderr}`);
2333
+ const hookUpdateSkippedJson = JSON.parse(hookUpdateSkippedResult.stdout);
2334
+ const hookUpdateSkippedContext = hookUpdateSkippedJson.hookSpecificOutput?.additionalContext || '';
2335
+ if (String(hookUpdateSkippedContext).includes('was skipped for this conversation')) throw new Error('selftest failed: current installed SKS kept stale skipped update context');
2336
+ const hookUpdateSkippedState = await readJson(path.join(hookUpdateSkippedTmp, '.sneakoscope', 'state', 'update-check.json'), {});
2337
+ if (hookUpdateSkippedState.skipped) throw new Error('selftest failed: stale skipped update state was not cleared after installed SKS became current');
2338
+ const hookUpdateOldTmp = tmpdir();
2339
+ await initProject(hookUpdateOldTmp, {});
2340
+ const hookUpdateOldPayload = JSON.stringify({ cwd: hookUpdateOldTmp, prompt: '상태 확인해줘' });
2341
+ const hookUpdateOldResult = await runProcess(process.execPath, [hookBin, 'hook', 'user-prompt-submit'], {
2342
+ cwd: hookUpdateOldTmp,
2343
+ input: hookUpdateOldPayload,
2344
+ env: { SKS_NPM_VIEW_SNEAKOSCOPE_VERSION: '9.9.9', SKS_INSTALLED_SKS_VERSION: '0.0.0' },
2345
+ timeoutMs: 15000,
2346
+ maxOutputBytes: 256 * 1024
2347
+ });
2348
+ if (hookUpdateOldResult.code !== 0) throw new Error(`selftest failed: stale update hook exited ${hookUpdateOldResult.code}: ${hookUpdateOldResult.stderr}`);
2349
+ const hookUpdateOldJson = JSON.parse(hookUpdateOldResult.stdout);
2350
+ const hookUpdateOldContext = hookUpdateOldJson.hookSpecificOutput?.additionalContext || '';
2351
+ if (!String(hookUpdateOldContext).includes('Update SKS now') || !String(hookUpdateOldContext).includes('Skip update for this conversation')) throw new Error('selftest failed: hook did not prompt when installed SKS is stale');
2352
+ const hookUpdateOldState = await readJson(path.join(hookUpdateOldTmp, '.sneakoscope', 'state', 'update-check.json'), {});
2353
+ if (hookUpdateOldState.pending_offer?.latest !== '9.9.9') throw new Error('selftest failed: stale installed SKS did not persist pending update offer');
2266
2354
  const hookKoreanSksTmp = tmpdir();
2267
2355
  await initProject(hookKoreanSksTmp, {});
2268
2356
  const hookKoreanSksPayload = JSON.stringify({ cwd: hookKoreanSksTmp, prompt: koreanReadmeInstallPrompt });
package/src/core/fsx.mjs CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
 
8
- export const PACKAGE_VERSION = '0.6.78';
8
+ export const PACKAGE_VERSION = '0.6.80';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
 
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { projectRoot, readJson, readText, writeJsonAtomic, appendJsonl, readStdin, nowIso, runProcess, which, PACKAGE_VERSION, sha256 } from './fsx.mjs';
2
+ import { projectRoot, readJson, readText, writeJsonAtomic, appendJsonl, readStdin, nowIso, runProcess, which, PACKAGE_VERSION, sha256, packageRoot } from './fsx.mjs';
3
3
  import { looksInteractiveCommand, interactiveCommandReason } from './no-question-guard.mjs';
4
4
  import { missionDir, setCurrent, stateFile } from './mission.mjs';
5
5
  import { checkDbOperation, dbBlockReason, handleMadSksUserConfirmation } from './db-safety.mjs';
@@ -334,6 +334,50 @@ async function updateCheckContext(root, payload, prompt) {
334
334
  const updateState = await readJson(statePath, {});
335
335
  const conv = conversationId(payload);
336
336
  const pending = updateState.pending_offer;
337
+ let effective = null;
338
+ async function effectiveVersion() {
339
+ if (!effective) {
340
+ const installed = await detectInstalledSksVersion();
341
+ effective = {
342
+ installed,
343
+ current: highestVersion([PACKAGE_VERSION, installed.version])
344
+ };
345
+ }
346
+ return effective;
347
+ }
348
+ if (pending?.latest) {
349
+ const currentCheck = await effectiveVersion();
350
+ if (compareVersions(pending.latest, currentCheck.current) <= 0) {
351
+ await writeJsonAtomic(statePath, {
352
+ ...updateState,
353
+ current: currentCheck.current,
354
+ runtime_current: PACKAGE_VERSION,
355
+ installed_current: currentCheck.installed.version || null,
356
+ latest: pending.latest,
357
+ checked_at: nowIso(),
358
+ pending_offer: null,
359
+ check_error: null
360
+ });
361
+ return '';
362
+ }
363
+ }
364
+ if (updateState.skipped?.latest) {
365
+ const currentCheck = await effectiveVersion();
366
+ if (compareVersions(updateState.skipped.latest, currentCheck.current) <= 0) {
367
+ await writeJsonAtomic(statePath, {
368
+ ...updateState,
369
+ current: currentCheck.current,
370
+ runtime_current: PACKAGE_VERSION,
371
+ installed_current: currentCheck.installed.version || null,
372
+ latest: updateState.skipped.latest,
373
+ checked_at: nowIso(),
374
+ pending_offer: null,
375
+ skipped: null,
376
+ check_error: null
377
+ });
378
+ return '';
379
+ }
380
+ }
337
381
  if (pending?.conversation_id === conv && pending?.latest && looksLikeUpdateDecline(prompt)) {
338
382
  await writeJsonAtomic(statePath, {
339
383
  ...updateState,
@@ -354,26 +398,34 @@ async function updateCheckContext(root, payload, prompt) {
354
398
  return `SKS update check: update ${updateState.skipped.latest} was skipped for this conversation only. Do not ask again in this conversation; check again next conversation.`;
355
399
  }
356
400
  const check = await checkLatestVersion();
401
+ const { installed, current } = await effectiveVersion();
402
+ const isCurrent = check.latest && compareVersions(check.latest, current) <= 0;
357
403
  await writeJsonAtomic(statePath, {
358
404
  ...updateState,
359
- current: PACKAGE_VERSION,
405
+ current,
406
+ runtime_current: PACKAGE_VERSION,
407
+ installed_current: installed.version || null,
360
408
  latest: check.latest || null,
361
409
  checked_at: nowIso(),
410
+ pending_offer: isCurrent ? null : updateState.pending_offer || null,
362
411
  check_error: check.error || null
363
412
  });
364
- if (!check.latest || check.error || compareVersions(check.latest, PACKAGE_VERSION) <= 0) return '';
413
+ if (!check.latest || check.error || isCurrent) return '';
365
414
  await writeJsonAtomic(statePath, {
366
415
  ...updateState,
367
- current: PACKAGE_VERSION,
416
+ current,
417
+ runtime_current: PACKAGE_VERSION,
418
+ installed_current: installed.version || null,
368
419
  latest: check.latest,
369
420
  checked_at: nowIso(),
370
421
  pending_offer: { conversation_id: conv, latest: check.latest, offered_at: nowIso() },
371
422
  skipped: updateState.skipped?.conversation_id === conv ? null : updateState.skipped || null
372
423
  });
373
- return `SKS update check: installed ${PACKAGE_VERSION}, latest ${check.latest}. Before any other work, ask the user to choose: "Update SKS now" or "Skip update for this conversation". If they choose update, run npm i -g sneakoscope for global installs, or npm i -D sneakoscope && npx sks setup --install-scope project for project installs, then run sks setup and sks doctor --fix. If they skip, do not ask again in this conversation, but check again next conversation.`;
424
+ return `SKS update check: installed ${current}, latest ${check.latest}. Before any other work, ask the user to choose: "Update SKS now" or "Skip update for this conversation". If they choose update, run npm i -g sneakoscope for global installs, or npm i -D sneakoscope && npx sks setup --install-scope project for project installs, then run sks setup and sks doctor --fix. If they skip, do not ask again in this conversation, but check again next conversation.`;
374
425
  }
375
426
 
376
427
  async function checkLatestVersion() {
428
+ if (process.env.SKS_NPM_VIEW_SNEAKOSCOPE_VERSION) return { latest: process.env.SKS_NPM_VIEW_SNEAKOSCOPE_VERSION };
377
429
  const npm = await which('npm').catch(() => null);
378
430
  if (!npm) return { error: 'npm not found' };
379
431
  const result = await runProcess(npm, ['view', 'sneakoscope', 'version'], { timeoutMs: 3500, maxOutputBytes: 4096 });
@@ -381,6 +433,33 @@ async function checkLatestVersion() {
381
433
  return { latest: result.stdout.trim().split(/\s+/).pop() };
382
434
  }
383
435
 
436
+ async function detectInstalledSksVersion() {
437
+ const override = parseVersionText(process.env.SKS_INSTALLED_SKS_VERSION || '');
438
+ if (override) return { version: override, source: 'env' };
439
+ const candidates = [];
440
+ const pkg = await readJson(path.join(packageRoot(), 'package.json'), {}).catch(() => ({}));
441
+ if (parseVersionText(pkg.version)) candidates.push({ version: parseVersionText(pkg.version), source: 'package.json' });
442
+ const sks = await which('sks').catch(() => null);
443
+ if (!sks) return candidates[0] || { version: null, source: null };
444
+ const result = await runProcess(sks, ['--version'], {
445
+ timeoutMs: 2000,
446
+ maxOutputBytes: 4096,
447
+ env: { SKS_DISABLE_UPDATE_CHECK: '1' }
448
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err.message }));
449
+ if (result.code === 0 && parseVersionText(result.stdout)) candidates.push({ version: parseVersionText(result.stdout), source: sks });
450
+ if (candidates.length) return candidates.reduce((best, candidate) => compareVersions(candidate.version, best.version) > 0 ? candidate : best);
451
+ return { version: null, source: sks, error: `${result.stderr || result.stdout || 'sks --version failed'}`.trim() };
452
+ }
453
+
454
+ function parseVersionText(text) {
455
+ const match = String(text || '').match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/);
456
+ return match ? match[0] : null;
457
+ }
458
+
459
+ function highestVersion(versions = []) {
460
+ return versions.filter(Boolean).reduce((best, candidate) => compareVersions(candidate, best) > 0 ? candidate : best, '0.0.0');
461
+ }
462
+
384
463
  function compareVersions(a, b) {
385
464
  const pa = String(a || '').split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
386
465
  const pb = String(b || '').split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);