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.
- package/cli.js +1 -0
- package/concepts/ar/UltraPlan.md +9 -9
- package/concepts/da/UltraPlan.md +9 -9
- package/concepts/de/UltraPlan.md +9 -9
- package/concepts/en/UltraPlan.md +9 -9
- package/concepts/es/UltraPlan.md +9 -9
- package/concepts/fr/UltraPlan.md +9 -9
- package/concepts/it/UltraPlan.md +9 -9
- package/concepts/ja/UltraPlan.md +9 -9
- package/concepts/ko/UltraPlan.md +9 -9
- package/concepts/no/UltraPlan.md +9 -9
- package/concepts/pl/UltraPlan.md +9 -9
- package/concepts/pt-BR/UltraPlan.md +9 -9
- package/concepts/ru/UltraPlan.md +9 -9
- package/concepts/th/UltraPlan.md +9 -9
- package/concepts/tr/UltraPlan.md +9 -9
- package/concepts/uk/UltraPlan.md +9 -9
- package/concepts/zh/UltraPlan.md +9 -9
- package/concepts/zh-TW/UltraPlan.md +9 -9
- package/dist/assets/App-BvVdj099.css +1 -0
- package/dist/assets/App-CYdOioAz.js +2 -0
- package/dist/assets/{MdxEditorPanel-DI3B4gsM.js → MdxEditorPanel-CuCAe1OG.js} +1 -1
- package/dist/assets/Mobile-CfNYOgRa.js +1 -0
- package/dist/assets/{ProxyStatsModal-B8K0AtAq.js → ProxyStatsModal-Dhj4EVnh.js} +1 -1
- package/dist/assets/index-sgW8MlD3.js +2 -0
- package/dist/assets/seqResourceLoaders-B-5xGs9q.css +41 -0
- package/dist/assets/seqResourceLoaders-Cj0pdz-l.js +2 -0
- package/dist/index.html +1 -1
- package/node_modules/@ccv/core/src/contentFilter.js +3 -0
- package/node_modules/@ccv/core/src/queued-prompt.js +66 -0
- package/node_modules/@ccv/core/src/v2-transcript-normalizer.js +78 -14
- package/package.json +1 -1
- package/server/lib/ask/perm-bridge.js +4 -1
- package/server/lib/auto-approve.js +160 -0
- package/server/lib/chat-queue.js +350 -120
- package/server/lib/file-access-policy.js +24 -2
- package/server/lib/scheduled-task-executor.js +3 -1
- package/server/lib/sdk-manager.js +377 -76
- package/server/lib/v2/conversation-store.js +15 -11
- package/server/lib/v2/v2-writer.js +1 -1
- package/server/lib/view-root.js +4 -4
- package/server/pty-manager.js +8 -8
- package/server/routes/ask-perm.js +54 -3
- package/server/routes/files-content.js +352 -16
- package/server/routes/files-fs.js +122 -36
- package/server/routes/resume.js +1 -1
- package/server/routes/sdk-context.js +13 -2
- package/server/routes/skills.js +19 -8
- package/server/server.js +205 -34
- package/server/system-prompt-templates/presets/kimi-k3.md +1 -0
- package/ultraAgents/code-expert.json +1 -1
- package/dist/assets/App-BzV1ieDH.js +0 -2
- package/dist/assets/App-DU5jmGrx.css +0 -1
- package/dist/assets/Mobile-qBkAnXy3.js +0 -1
- package/dist/assets/index-CU0gt6m1.js +0 -2
- package/dist/assets/seqResourceLoaders-CxUFE36z.css +0 -41
- 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
|
|
39
|
-
* additionally hash the FULL message JSON.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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.
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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 =
|
|
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.
|
package/server/lib/view-root.js
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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).
|
package/server/pty-manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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({
|
|
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 });
|