cc-viewer 1.9.14 → 1.9.16

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.
Files changed (57) hide show
  1. package/cli.js +1 -0
  2. package/concepts/ar/UltraPlan.md +9 -9
  3. package/concepts/da/UltraPlan.md +9 -9
  4. package/concepts/de/UltraPlan.md +9 -9
  5. package/concepts/en/UltraPlan.md +9 -9
  6. package/concepts/es/UltraPlan.md +9 -9
  7. package/concepts/fr/UltraPlan.md +9 -9
  8. package/concepts/it/UltraPlan.md +9 -9
  9. package/concepts/ja/UltraPlan.md +9 -9
  10. package/concepts/ko/UltraPlan.md +9 -9
  11. package/concepts/no/UltraPlan.md +9 -9
  12. package/concepts/pl/UltraPlan.md +9 -9
  13. package/concepts/pt-BR/UltraPlan.md +9 -9
  14. package/concepts/ru/UltraPlan.md +9 -9
  15. package/concepts/th/UltraPlan.md +9 -9
  16. package/concepts/tr/UltraPlan.md +9 -9
  17. package/concepts/uk/UltraPlan.md +9 -9
  18. package/concepts/zh/UltraPlan.md +9 -9
  19. package/concepts/zh-TW/UltraPlan.md +9 -9
  20. package/dist/assets/App-BvVdj099.css +1 -0
  21. package/dist/assets/App-CYdOioAz.js +2 -0
  22. package/dist/assets/{MdxEditorPanel-DI3B4gsM.js → MdxEditorPanel-CuCAe1OG.js} +1 -1
  23. package/dist/assets/Mobile-CfNYOgRa.js +1 -0
  24. package/dist/assets/{ProxyStatsModal-B8K0AtAq.js → ProxyStatsModal-Dhj4EVnh.js} +1 -1
  25. package/dist/assets/index-sgW8MlD3.js +2 -0
  26. package/dist/assets/seqResourceLoaders-B-5xGs9q.css +41 -0
  27. package/dist/assets/seqResourceLoaders-Cj0pdz-l.js +2 -0
  28. package/dist/index.html +1 -1
  29. package/node_modules/@ccv/core/src/contentFilter.js +3 -0
  30. package/node_modules/@ccv/core/src/queued-prompt.js +66 -0
  31. package/node_modules/@ccv/core/src/v2-transcript-normalizer.js +78 -14
  32. package/package.json +1 -1
  33. package/server/lib/ask/perm-bridge.js +4 -1
  34. package/server/lib/auto-approve.js +160 -0
  35. package/server/lib/chat-queue.js +350 -120
  36. package/server/lib/file-access-policy.js +24 -2
  37. package/server/lib/scheduled-task-executor.js +3 -1
  38. package/server/lib/sdk-manager.js +377 -76
  39. package/server/lib/v2/conversation-store.js +15 -11
  40. package/server/lib/v2/v2-writer.js +1 -1
  41. package/server/lib/view-root.js +4 -4
  42. package/server/pty-manager.js +8 -8
  43. package/server/routes/ask-perm.js +54 -3
  44. package/server/routes/files-content.js +352 -16
  45. package/server/routes/files-fs.js +122 -36
  46. package/server/routes/resume.js +1 -1
  47. package/server/routes/sdk-context.js +13 -2
  48. package/server/routes/skills.js +19 -8
  49. package/server/server.js +205 -34
  50. package/server/system-prompt-templates/presets/kimi-k3.md +1 -0
  51. package/ultraAgents/code-expert.json +1 -1
  52. package/dist/assets/App-BzV1ieDH.js +0 -2
  53. package/dist/assets/App-DU5jmGrx.css +0 -1
  54. package/dist/assets/Mobile-qBkAnXy3.js +0 -1
  55. package/dist/assets/index-CU0gt6m1.js +0 -2
  56. package/dist/assets/seqResourceLoaders-CxUFE36z.css +0 -41
  57. package/dist/assets/seqResourceLoaders-nGCs11zu.js +0 -2
@@ -35,13 +35,13 @@ export class ConversationStore {
35
35
  /**
36
36
  * @param {object} paths - sessionPaths() object
37
37
  * @param {{appendTo: Function}} queue - AsyncWriteQueue (explicit-path API)
38
- * @param {{exactFps?: boolean}} [opts] - exact mode: judgement fingerprints
39
- * additionally hash the FULL message JSON. The offline converter needs
40
- * byte-grade faithfulness (its golden gate replays byte-identical wire);
41
- * fingerprintMsg's 80-char first-block snip cannot distinguish wires that
42
- * share a long prefix (real data 2026-07-14: sub prompts identical in the
43
- * first 80 chars, differing later, judged "unchanged" → replay stuck on
44
- * the first wire). The live request path keeps the cheap default.
38
+ * @param {{exactFps?: boolean|'main'}} [opts] - exact mode: judgement
39
+ * fingerprints additionally hash the FULL normalized message JSON. Default
40
+ * 'main' covers the user-facing conversation; sub/misc retain cheap fps.
41
+ * The offline converter passes true for byte-grade faithfulness across all
42
+ * conversations. fingerprintMsg's 80-char first-block snip cannot see an
43
+ * appended prompt in a later content block, which would otherwise be
44
+ * judged "unchanged" and vanish from the replayed main conversation.
45
45
  */
46
46
  constructor(paths, queue, opts = {}) {
47
47
  this._paths = paths;
@@ -54,9 +54,12 @@ export class ConversationStore {
54
54
  // per request on affected conversations (real-data storm, ~62% of corpus
55
55
  // bytes). Storage still records the wire verbatim; only the judgement
56
56
  // compares modulo cache_control/form.
57
- this._fpOf = opts.exactFps
58
- ? (m) => { const n = normalizeMsgForEquality(m); return `${fingerprintMsg(n)}:${fnv1a(JSON.stringify(n))}`; }
59
- : fingerprintMsg;
57
+ this._exactFps = opts.exactFps === undefined ? 'main' : opts.exactFps;
58
+ this._exactFpOf = (m) => {
59
+ const n = normalizeMsgForEquality(m);
60
+ return `${fingerprintMsg(n)}:${fnv1a(JSON.stringify(n))}`;
61
+ };
62
+ this._fpOf = this._exactFps === true ? this._exactFpOf : fingerprintMsg;
60
63
  }
61
64
 
62
65
  _state(convKey) {
@@ -123,7 +126,8 @@ export class ConversationStore {
123
126
  // exactly like v1's Plan C does, or the two dual-write sides legitimately
124
127
  // diverge in a way the verify digest (built on messageFingerprint) is
125
128
  // structurally blind to (review P2).
126
- const fpsIn = msgs.map(this._fpOf);
129
+ const fpOf = this._exactFps === 'main' && convKey === 'main' ? this._exactFpOf : this._fpOf;
130
+ const fpsIn = msgs.map(fpOf);
127
131
  const prev = st.count;
128
132
  const prevFps = st.fps;
129
133
 
@@ -73,7 +73,7 @@ export class V2Writer {
73
73
  // writers pass none of these.
74
74
  this._sessionsDirName = opts.sessionsDirName || 'sessions';
75
75
  this._metaExtra = opts.metaExtra || null;
76
- this._exactConvFps = !!opts.exactConvFps;
76
+ this._exactConvFps = opts.exactConvFps;
77
77
  // S6b live feed: called with the session dir after every ingest so the
78
78
  // in-process live cursor can read the fresh appends with zero fs-watch
79
79
  // latency. Purely a nudge — the feed's data source stays the files.
@@ -1,7 +1,6 @@
1
1
  // View-root resolution (multi-project, 2026-10): map an optional `?project=`
2
2
  // name to the REAL filesystem root of that project, so project-scoped READ
3
- // routes (file tree / git / skills / memory / stats / search) can follow the
4
- // viewed parallel project instead of always the bound one.
3
+ // routes and boundary-checked file mutations follow the viewed project.
5
4
  //
6
5
  // Name → path sources (both consulted, in order):
7
6
  // 1. workspace-registry (loadWorkspaces) — persistent { path, projectName },
@@ -22,7 +21,8 @@
22
21
  // 3. still >1 real candidate → 400, never a silent guess.
23
22
  //
24
23
  // Pure + dependency-injected (fs/registry/pty seams) so it is directly
25
- // node-testable. WRITE routes must NOT use this — they stay bound-only.
24
+ // node-testable. File mutation routes use the same resolver as their reads;
25
+ // every target must still pass the resolved root's filesystem boundary checks.
26
26
 
27
27
  import { realpathSync, existsSync, statSync } from 'node:fs';
28
28
  import { projectKeyForCwd } from './system-prompt-snapshots.js';
@@ -192,7 +192,7 @@ export function resolveViewRoot({ projectParam, instanceParam, boundCwd, loadWor
192
192
  }
193
193
 
194
194
  /**
195
- * HTTP-facing wrapper shared by every project-scoped READ route (multi-project,
195
+ * HTTP-facing wrapper shared by project-scoped routes (multi-project,
196
196
  * 2026-10): resolves `?project=` (or a POST-body override) via resolveViewRoot
197
197
  * and, on failure, writes the error response itself. Returns the resolved root
198
198
  * to continue with, or null after replying (the caller must return early).
@@ -1290,12 +1290,15 @@ export function resolveInstanceForCwd(cwd) {
1290
1290
  * would make resize/scrollback/getPtyState follow whichever project was last
1291
1291
  * SENT to, a new cross-project bleed (review P1, 2026-10-05).
1292
1292
  */
1293
- function _resolveKeyByAnchor({ sessionId, project, instanceKey } = {}) {
1293
+ function _resolveKeyByAnchor({ sessionId, project, instanceKey, strict = false } = {}) {
1294
+ // Explicit strict targets never fall back to the active conversation. Without this,
1295
+ // a dead project/instance could receive a queued prompt meant for another one.
1294
1296
  // Multi-instance: an explicit instanceKey is the strongest anchor — it pins THIS exact
1295
1297
  // process (disambiguating two same-cwd instances that basename routing cannot).
1296
1298
  if (typeof instanceKey === 'string' && /^ccv-[0-9a-f]+$/.test(instanceKey)) {
1297
1299
  const rec = ptys.get(instanceKey);
1298
1300
  if (rec && rec.ptyProcess) return instanceKey;
1301
+ if (strict) return null;
1299
1302
  }
1300
1303
  const projKey = (typeof project === 'string' && project) ? _resolveKey({ project }) : null;
1301
1304
  if (typeof sessionId === 'string' && sessionId) {
@@ -1313,12 +1316,13 @@ function _resolveKeyByAnchor({ sessionId, project, instanceKey } = {}) {
1313
1316
  }
1314
1317
  }
1315
1318
  if (projKey) return projKey;
1319
+ if (strict) return null;
1316
1320
  return activePtyKey != null && ptys.has(activePtyKey) ? activePtyKey : null;
1317
1321
  }
1318
1322
 
1319
1323
  /** writeToPty variant routed by an explicit anchor instead of the global active pointer. */
1320
- export function writeToPtyFor(data, { sessionId, project, instanceKey } = {}) {
1321
- const key = _resolveKeyByAnchor({ sessionId, project, instanceKey });
1324
+ export function writeToPtyFor(data, { sessionId, project, instanceKey, strict = false } = {}) {
1325
+ const key = _resolveKeyByAnchor({ sessionId, project, instanceKey, strict });
1322
1326
  const s = key ? ptys.get(key) : null;
1323
1327
  if (s && s.ptyProcess) {
1324
1328
  s.ptyProcess.write(data);
@@ -1332,11 +1336,7 @@ export function writeToPtyFor(data, { sessionId, project, instanceKey } = {}) {
1332
1336
  * the write fails (onComplete(false)) WITHOUT falling back to the project/active record —
1333
1337
  * scheduled-task injection must never land in the wrong conversation (review P0-2). */
1334
1338
  export function writeToPtySequentialFor(chunks, onComplete, opts = {}, anchor = {}) {
1335
- let key = _resolveKeyByAnchor(anchor);
1336
- if (opts && opts.strict && typeof anchor.instanceKey === 'string' && anchor.instanceKey) {
1337
- const rec = ptys.get(anchor.instanceKey);
1338
- if (!rec || !rec.ptyProcess) key = null; // strict: a dead explicit instance never falls back
1339
- }
1339
+ const key = _resolveKeyByAnchor({ ...anchor, strict: opts.strict === true });
1340
1340
  const s = key ? ptys.get(key) : null;
1341
1341
  if (!s || !s.ptyProcess || !chunks || chunks.length === 0) {
1342
1342
  if (onComplete) onComplete(false);
@@ -7,6 +7,8 @@ import { _projectName } from '../interceptor.js';
7
7
  import { readPrefsRaw } from '../lib/prefs-store.js';
8
8
  import { getCurrentProjectKey, hasFork, resolveScoped } from '../lib/project-prefs.js';
9
9
  import { reportSwallowed } from '../lib/error-report.js';
10
+ import { computeAutoApprove, createAutoApproveState } from '../lib/auto-approve.js';
11
+ import { isPublishCommand } from '../lib/approval-policy.js';
10
12
 
11
13
  function pendingAsks(req, res, parsedUrl, isLocal, deps) {
12
14
  try {
@@ -394,7 +396,7 @@ function permHook(req, res, parsedUrl, isLocal, deps) {
394
396
  return;
395
397
  }
396
398
  try {
397
- const { toolName, input } = JSON.parse(body);
399
+ const { toolName, input, cwd } = JSON.parse(body);
398
400
  if (!toolName) {
399
401
  res.writeHead(400, { 'Content-Type': 'application/json' });
400
402
  res.end(JSON.stringify({ error: 'Missing toolName' }));
@@ -407,6 +409,7 @@ function permHook(req, res, parsedUrl, isLocal, deps) {
407
409
  const oldest = deps.pendingPermHooks.get(oldestId);
408
410
  if (oldest) {
409
411
  clearTimeout(oldest.timer);
412
+ if (oldest.auto?.timer) clearTimeout(oldest.auto.timer);
410
413
  try { if (!oldest.res.headersSent) { oldest.res.writeHead(429, { 'Content-Type': 'application/json' }); oldest.res.end(JSON.stringify({ error: 'Too many concurrent requests' })); } } catch {}
411
414
  deps.pendingPermHooks.delete(oldestId);
412
415
  }
@@ -431,6 +434,7 @@ function permHook(req, res, parsedUrl, isLocal, deps) {
431
434
  const timer = setTimeout(() => {
432
435
  const entry = deps.pendingPermHooks.get(id);
433
436
  if (entry) {
437
+ if (entry.auto?.timer) clearTimeout(entry.auto.timer);
434
438
  deps.pendingPermHooks.delete(id);
435
439
  try {
436
440
  if (!entry.res.headersSent) {
@@ -447,11 +451,33 @@ function permHook(req, res, parsedUrl, isLocal, deps) {
447
451
  }
448
452
  }, PERM_HOOK_TIMEOUT);
449
453
 
450
- deps.pendingPermHooks.set(id, { toolName, input, res, timer, createdAt: Date.now() });
454
+ // Server-side auto-approve countdown (same semantics as the SDK path in
455
+ // sdk-manager.js): armed from request arrival; the server answers the hook
456
+ // itself at deadline + 1s buffer when no client did. autoCancelled is set by
457
+ // the perm-hook-auto-cancel WS handler (the approval then stays pending for
458
+ // a manual decision) and replayed to fresh clients.
459
+ // npm publish is a hard safety floor: always wait for a human decision, never
460
+ // the server-side auto-approve fallback (SDK path parity: sdk-manager.js).
461
+ const autoMeta = isPublishCommand(toolName, input)
462
+ ? { enabled: false }
463
+ : computeAutoApprove('perm', typeof cwd === 'string' && cwd ? cwd : undefined, { file: deps.getPrefsFile?.() });
464
+
465
+ const hookEntry = {
466
+ toolName, input, res, timer, createdAt: Date.now(),
467
+ autoCancelled: false,
468
+ auto: null,
469
+ };
470
+ deps.pendingPermHooks.set(id, hookEntry);
451
471
 
452
472
  // Broadcast to all terminal WS clients
453
473
  if (deps.terminalWss) {
454
- const pmsg = JSON.stringify({ type: 'perm-hook-pending', id, toolName, input });
474
+ const pmsg = JSON.stringify({
475
+ type: 'perm-hook-pending', id, toolName, input,
476
+ // Always stamp autoApproveSeconds (0 = server says OFF) so a client whose
477
+ // global pref disagrees with the fork-resolved value never re-arms locally.
478
+ autoApproveSeconds: autoMeta.enabled ? autoMeta.seconds : 0,
479
+ ...(autoMeta.enabled ? { autoApproveAt: autoMeta.autoApproveAt } : {}),
480
+ });
455
481
  deps.terminalWss.clients.forEach((client) => {
456
482
  if (client.readyState === 1) {
457
483
  try { client.send(pmsg); } catch {}
@@ -459,11 +485,36 @@ function permHook(req, res, parsedUrl, isLocal, deps) {
459
485
  });
460
486
  }
461
487
 
488
+ // Arm AFTER the entry is in the map AND the pending frame is out: instant (-1)
489
+ // fires synchronously and resolves through the entry, so clients observe the
490
+ // order pending → resolved (never a ghost panel for an already-answered hook).
491
+ if (autoMeta.enabled) {
492
+ hookEntry.auto = createAutoApproveState(autoMeta, () => {
493
+ const entry = deps.pendingPermHooks.get(id);
494
+ if (!entry || entry.autoCancelled) return;
495
+ clearTimeout(entry.timer);
496
+ deps.pendingPermHooks.delete(id);
497
+ try {
498
+ if (!entry.res.headersSent) {
499
+ entry.res.writeHead(200, { 'Content-Type': 'application/json' });
500
+ entry.res.end(JSON.stringify({ decision: 'allow' }));
501
+ }
502
+ } catch {}
503
+ if (deps.terminalWss) {
504
+ const rmsg = JSON.stringify({ type: 'perm-hook-resolved', id, reason: 'auto-approved' });
505
+ deps.terminalWss.clients.forEach((c) => {
506
+ if (c.readyState === 1) try { c.send(rmsg); } catch {}
507
+ });
508
+ }
509
+ });
510
+ }
511
+
462
512
  // Handle perm-bridge.js disconnection
463
513
  res.on('close', () => {
464
514
  const entry = deps.pendingPermHooks.get(id);
465
515
  if (entry) {
466
516
  clearTimeout(entry.timer);
517
+ if (entry.auto?.timer) clearTimeout(entry.auto.timer);
467
518
  deps.pendingPermHooks.delete(id);
468
519
  if (deps.terminalWss) {
469
520
  const tmsg = JSON.stringify({ type: 'perm-hook-timeout', id });