chat-recall 0.5.6 → 0.5.8
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/README.md +332 -0
- package/dist/cli.js +7057 -4542
- package/dist/cli.js.map +1 -1
- package/dist/code-index-cursor.d.ts +18 -0
- package/dist/code-index-cursor.d.ts.map +1 -0
- package/dist/code-index-cursor.js +89 -0
- package/dist/code-index-cursor.js.map +1 -0
- package/dist/daemon-log.d.ts +44 -0
- package/dist/daemon-log.d.ts.map +1 -0
- package/dist/daemon-log.js +74 -0
- package/dist/daemon-log.js.map +1 -0
- package/dist/derived.d.ts +20 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +106 -0
- package/dist/derived.js.map +1 -0
- package/dist/install-skills.d.ts.map +1 -1
- package/dist/install-skills.js +6 -1
- package/dist/install-skills.js.map +1 -1
- package/dist/log-rotate.d.ts +16 -0
- package/dist/log-rotate.d.ts.map +1 -0
- package/dist/log-rotate.js +50 -0
- package/dist/log-rotate.js.map +1 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +5865 -3789
- package/dist/mcp.js.map +1 -1
- package/dist/project-tasks.js +1 -1
- package/dist/project-tasks.js.map +1 -1
- package/dist/redact-deep.d.ts +4 -0
- package/dist/redact-deep.d.ts.map +1 -0
- package/dist/redact-deep.js +26 -0
- package/dist/redact-deep.js.map +1 -0
- package/dist/scan-pool.d.ts +56 -0
- package/dist/scan-pool.d.ts.map +1 -0
- package/dist/scan-pool.js +245 -0
- package/dist/scan-pool.js.map +1 -0
- package/dist/scan-worker.d.ts +78 -0
- package/dist/scan-worker.d.ts.map +1 -0
- package/dist/scan-worker.js +11175 -0
- package/dist/scan-worker.js.map +1 -0
- package/dist/self-restart.d.ts +20 -2
- package/dist/self-restart.d.ts.map +1 -1
- package/dist/self-restart.js +16 -6
- package/dist/self-restart.js.map +1 -1
- package/dist/server-probe.d.ts +76 -0
- package/dist/server-probe.d.ts.map +1 -0
- package/dist/server-probe.js +106 -0
- package/dist/server-probe.js.map +1 -0
- package/dist/sync-client.d.ts +24 -0
- package/dist/sync-client.d.ts.map +1 -1
- package/dist/sync-client.js +496 -151
- package/dist/sync-client.js.map +1 -1
- package/dist/target-breaker.d.ts +61 -0
- package/dist/target-breaker.d.ts.map +1 -0
- package/dist/target-breaker.js +85 -0
- package/dist/target-breaker.js.map +1 -0
- package/dist/telemetry-consent.d.ts +31 -0
- package/dist/telemetry-consent.d.ts.map +1 -0
- package/dist/telemetry-consent.js +115 -0
- package/dist/telemetry-consent.js.map +1 -0
- package/dist/telemetry.d.ts +22 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +125 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tick-queue.d.ts +67 -0
- package/dist/tick-queue.d.ts.map +1 -0
- package/dist/tick-queue.js +104 -0
- package/dist/tick-queue.js.map +1 -0
- package/dist/transport-safety.d.ts +43 -0
- package/dist/transport-safety.d.ts.map +1 -0
- package/dist/transport-safety.js +100 -0
- package/dist/transport-safety.js.map +1 -0
- package/dist/verify-repair.js +1 -1
- package/dist/verify-repair.js.map +1 -1
- package/dist/watch.js +8069 -5720
- package/package.json +21 -2
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this machine may report operational telemetry, and what counts as it.
|
|
3
|
+
*
|
|
4
|
+
* TWO INDEPENDENT GATES, both of which must open. They are separate on purpose:
|
|
5
|
+
* one answers "is the user willing", the other "is the operator entitled", and
|
|
6
|
+
* conflating them is how a product ends up collecting from someone who said no.
|
|
7
|
+
*
|
|
8
|
+
* 1. CONSENT — the user's setting, checked on this machine. It wins over
|
|
9
|
+
* everything. No server response can override it, because a server should
|
|
10
|
+
* not be able to talk a client into sending data its owner declined.
|
|
11
|
+
*
|
|
12
|
+
* 2. ELIGIBILITY — the server says whether this tenant's plan includes it.
|
|
13
|
+
* The client cannot know its own plan (the entitlement lives in the control
|
|
14
|
+
* plane), so it is told, and it defaults to NOT eligible: an older server
|
|
15
|
+
* that says nothing gets nothing.
|
|
16
|
+
*
|
|
17
|
+
* The server enforces (2) again at ingest. This gate is an optimisation — it
|
|
18
|
+
* stops the data leaving a machine that shouldn't send it — not the boundary.
|
|
19
|
+
*
|
|
20
|
+
* ── What may be reported, and what may never be ──────────────────────────
|
|
21
|
+
* Operational facts about the COLLECTOR, never about the user's work:
|
|
22
|
+
*
|
|
23
|
+
* allowed walk duration, sessions considered/shipped, bytes uploaded,
|
|
24
|
+
* 429/failure counts, breaker trips, RSS peak, CLI version, OS,
|
|
25
|
+
* error CLASS plus a redacted message
|
|
26
|
+
* forbidden transcript content, prompts, file paths, project names, session
|
|
27
|
+
* ids, repository names, anything derived from what the user wrote
|
|
28
|
+
*
|
|
29
|
+
* `assertNoSensitiveKeys` enforces the second list structurally rather than by
|
|
30
|
+
* review, because "no paths" is exactly the promise that erodes one convenient
|
|
31
|
+
* field at a time.
|
|
32
|
+
*/
|
|
33
|
+
import { loadSettings } from '@chat-recall/engine/core/settings.js';
|
|
34
|
+
/** Env kill-switch, for a machine where editing settings is not practical. */
|
|
35
|
+
const ENV_OFF = () => {
|
|
36
|
+
const v = (process.env.CHAT_RECALL_TELEMETRY || '').trim().toLowerCase();
|
|
37
|
+
return v === '0' || v === 'off' || v === 'false' || v === 'no';
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Has the user opted out?
|
|
41
|
+
*
|
|
42
|
+
* Default is ON for the operational facts above — an opt-OUT, which is what the
|
|
43
|
+
* product intends and what the docs must say plainly. Anything that touches user
|
|
44
|
+
* CONTENT is not covered by this and is not collected at all, at any setting.
|
|
45
|
+
*/
|
|
46
|
+
export function userConsents() {
|
|
47
|
+
if (ENV_OFF())
|
|
48
|
+
return false;
|
|
49
|
+
try {
|
|
50
|
+
const s = loadSettings();
|
|
51
|
+
return s.privacy?.telemetry !== false;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Unreadable settings must not be read as consent.
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Eligibility as last reported by a server, keyed by server URL. */
|
|
59
|
+
const eligible = new Map();
|
|
60
|
+
/** Record what the server said about this tenant's plan. */
|
|
61
|
+
export function setTelemetryEligible(serverUrl, allowed) {
|
|
62
|
+
eligible.set(serverUrl.replace(/\/+$/, ''), allowed);
|
|
63
|
+
}
|
|
64
|
+
/** Default NO: a server that never said yes never gets telemetry. */
|
|
65
|
+
export function serverAllowsTelemetry(serverUrl) {
|
|
66
|
+
return eligible.get(serverUrl.replace(/\/+$/, '')) === true;
|
|
67
|
+
}
|
|
68
|
+
/** Test seam. */
|
|
69
|
+
export function _resetTelemetryEligibility() { eligible.clear(); }
|
|
70
|
+
/** Both gates, for one target. */
|
|
71
|
+
export function mayReport(serverUrl) {
|
|
72
|
+
return userConsents() && serverAllowsTelemetry(serverUrl);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Keys that must never appear in a telemetry payload.
|
|
76
|
+
*
|
|
77
|
+
* Checked structurally so the promise survives the next person adding "just the
|
|
78
|
+
* project name for context". A violation throws in development and drops the
|
|
79
|
+
* event in production — never sends it anyway.
|
|
80
|
+
*/
|
|
81
|
+
const FORBIDDEN_KEY = /(path|dir|file|project|repo|session|prompt|content|text|title|branch|url|email|user|home|cwd)/i;
|
|
82
|
+
/** Every forbidden key in `obj`, deeply. Empty means safe to send. */
|
|
83
|
+
export function findSensitiveKeys(obj, where = '') {
|
|
84
|
+
const out = [];
|
|
85
|
+
const walk = (v, at) => {
|
|
86
|
+
if (!v || typeof v !== 'object')
|
|
87
|
+
return;
|
|
88
|
+
if (Array.isArray(v)) {
|
|
89
|
+
v.forEach((e, i) => walk(e, `${at}[${i}]`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const [k, val] of Object.entries(v)) {
|
|
93
|
+
if (FORBIDDEN_KEY.test(k))
|
|
94
|
+
out.push({ key: k, where: at || '(root)' });
|
|
95
|
+
walk(val, at ? `${at}.${k}` : k);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
walk(obj, where);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Throw when a payload carries a forbidden key.
|
|
103
|
+
*
|
|
104
|
+
* Deliberately loud: a telemetry event that leaks a file path is a privacy
|
|
105
|
+
* incident, and the cheapest place to stop it is the type of failure a developer
|
|
106
|
+
* cannot ignore.
|
|
107
|
+
*/
|
|
108
|
+
export function assertNoSensitiveKeys(payload) {
|
|
109
|
+
const bad = findSensitiveKeys(payload);
|
|
110
|
+
if (bad.length > 0) {
|
|
111
|
+
throw new Error(`telemetry payload carries forbidden key(s): ${bad.map((b) => `${b.where}.${b.key}`).join(', ')}. `
|
|
112
|
+
+ 'Operational telemetry must describe the COLLECTOR, never the user\'s work.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=telemetry-consent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-consent.js","sourceRoot":"","sources":["../src/telemetry-consent.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** One reportable fact about a collector run. `kind` is the event class. */
|
|
2
|
+
export interface TelemetryEvent {
|
|
3
|
+
kind: string;
|
|
4
|
+
/** Numbers only, plus the few enumerated strings below. No free text. */
|
|
5
|
+
[k: string]: string | number | boolean | undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Record an event. Cheap and synchronous — it never blocks the caller and never
|
|
9
|
+
* throws, because a measurement must not be able to break the thing it measures.
|
|
10
|
+
*/
|
|
11
|
+
export declare function record(event: TelemetryEvent): void;
|
|
12
|
+
/** Pending events, for tests and `doctor`. */
|
|
13
|
+
export declare function pending(): readonly TelemetryEvent[];
|
|
14
|
+
export declare function _clearForTests(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Ship whatever is queued to every eligible target, then clear.
|
|
17
|
+
*
|
|
18
|
+
* Called after a sync completes — the connection is warm, the numbers are final,
|
|
19
|
+
* and there is no separate schedule to get wrong.
|
|
20
|
+
*/
|
|
21
|
+
export declare function flush(): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AAqCA,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;CACpD;AAKD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAWlD;AAED,8CAA8C;AAC9C,wBAAgB,OAAO,IAAI,SAAS,cAAc,EAAE,CAAkB;AACtE,wBAAgB,cAAc,IAAI,IAAI,CAAsB;AAE5D;;;;;GAKG;AACH,wBAAsB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAsC3C"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report how the COLLECTOR is doing, to the user's own server.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this exists ──────────────────────────────────────────────────────
|
|
5
|
+
* Every performance fact known about this product came from one developer
|
|
6
|
+
* machine. "How long does a first sync take for a customer with 5,000 sessions?"
|
|
7
|
+
* had no answer, because nothing measured a customer. The collector already
|
|
8
|
+
* computes everything needed — walk duration, sessions considered, bytes shipped,
|
|
9
|
+
* 429 counts, breaker trips, RSS peak — and then throws it away.
|
|
10
|
+
*
|
|
11
|
+
* ── What it is not ───────────────────────────────────────────────────────
|
|
12
|
+
* Not analytics, not usage tracking, not anything about what the user is doing.
|
|
13
|
+
* `assertNoSensitiveKeys` refuses a payload carrying a path, project, session id,
|
|
14
|
+
* prompt or content key, so the promise is enforced by the type of failure a
|
|
15
|
+
* developer cannot ignore rather than by review.
|
|
16
|
+
*
|
|
17
|
+
* ── Gates ────────────────────────────────────────────────────────────────
|
|
18
|
+
* Both must open (see telemetry-consent.ts): the user has not opted out, AND the
|
|
19
|
+
* server says this tenant's plan includes it. The server checks the plan again at
|
|
20
|
+
* ingest, so this gate exists to stop the data LEAVING a machine that should not
|
|
21
|
+
* send it — the client is never the boundary.
|
|
22
|
+
*
|
|
23
|
+
* ── Failure policy ───────────────────────────────────────────────────────
|
|
24
|
+
* Fire-and-forget, and silent. Telemetry that logs its own failures produces
|
|
25
|
+
* noise about the thing that was supposed to reduce noise, and a collector must
|
|
26
|
+
* never fail a sync because it could not report on the sync.
|
|
27
|
+
*/
|
|
28
|
+
import { hostname, platform } from 'os';
|
|
29
|
+
import { mayReport, assertNoSensitiveKeys } from './telemetry-consent.js';
|
|
30
|
+
import { fetchWithTimeout } from './http.js';
|
|
31
|
+
import { loadAllCredentials } from './sync-client.js';
|
|
32
|
+
const cliVersion = () => {
|
|
33
|
+
try {
|
|
34
|
+
return typeof __CLI_VERSION__ === 'string' ? __CLI_VERSION__ : 'dev';
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return 'dev';
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const MAX_QUEUE = 50;
|
|
41
|
+
const queue = [];
|
|
42
|
+
/**
|
|
43
|
+
* Record an event. Cheap and synchronous — it never blocks the caller and never
|
|
44
|
+
* throws, because a measurement must not be able to break the thing it measures.
|
|
45
|
+
*/
|
|
46
|
+
export function record(event) {
|
|
47
|
+
try {
|
|
48
|
+
assertNoSensitiveKeys(event);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
// A forbidden key is a developer error, not a runtime condition. Loud in
|
|
52
|
+
// development, dropped in production — never sent either way.
|
|
53
|
+
if (process.env.NODE_ENV !== 'production')
|
|
54
|
+
throw err;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (queue.length >= MAX_QUEUE)
|
|
58
|
+
queue.shift(); // newest matter most
|
|
59
|
+
queue.push(event);
|
|
60
|
+
}
|
|
61
|
+
/** Pending events, for tests and `doctor`. */
|
|
62
|
+
export function pending() { return queue; }
|
|
63
|
+
export function _clearForTests() { queue.length = 0; }
|
|
64
|
+
/**
|
|
65
|
+
* Ship whatever is queued to every eligible target, then clear.
|
|
66
|
+
*
|
|
67
|
+
* Called after a sync completes — the connection is warm, the numbers are final,
|
|
68
|
+
* and there is no separate schedule to get wrong.
|
|
69
|
+
*/
|
|
70
|
+
export async function flush() {
|
|
71
|
+
if (queue.length === 0)
|
|
72
|
+
return;
|
|
73
|
+
let targets = [];
|
|
74
|
+
try {
|
|
75
|
+
targets = loadAllCredentials();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const eligible = targets.filter((t) => mayReport(t.serverUrl));
|
|
81
|
+
if (eligible.length === 0) {
|
|
82
|
+
// Nothing may receive these. Drop them rather than growing a queue that will
|
|
83
|
+
// never drain — a user who opted out must not accumulate a backlog either.
|
|
84
|
+
queue.length = 0;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const events = queue.map((e) => ({
|
|
88
|
+
...e,
|
|
89
|
+
ts: Date.now(),
|
|
90
|
+
cliVersion: cliVersion(),
|
|
91
|
+
os: platform(),
|
|
92
|
+
// A STABLE, NON-IDENTIFYING machine marker. The server overwrites device_id
|
|
93
|
+
// from the authenticated token anyway (it is the only side that can resolve
|
|
94
|
+
// it), so this is a fallback for older servers and is deliberately a hash,
|
|
95
|
+
// never the hostname itself.
|
|
96
|
+
deviceId: hashHost(),
|
|
97
|
+
}));
|
|
98
|
+
queue.length = 0;
|
|
99
|
+
await Promise.allSettled(eligible.map(async (t) => {
|
|
100
|
+
try {
|
|
101
|
+
await fetchWithTimeout(`${t.serverUrl.replace(/\/+$/, '')}/api/client-events`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
'content-type': 'application/json',
|
|
105
|
+
...(t.token ? { authorization: `Bearer ${t.token}` } : {}),
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({ events }),
|
|
108
|
+
}, 10_000);
|
|
109
|
+
}
|
|
110
|
+
catch { /* fire and forget — see the header */ }
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
/** sha256 of the hostname, first 12 — stable per machine, not reversible. */
|
|
114
|
+
function hashHost() {
|
|
115
|
+
try {
|
|
116
|
+
// Lazy so the crypto import is not paid by callers that never report.
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
118
|
+
const { createHash } = require('node:crypto');
|
|
119
|
+
return createHash('sha256').update(hostname()).digest('hex').slice(0, 12);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One background task at a time, in priority order, with same-kind coalescing.
|
|
3
|
+
*
|
|
4
|
+
* ── What it replaces ─────────────────────────────────────────────────────
|
|
5
|
+
* Three independent in-flight booleans — `syncInFlight`, `intentDrainInFlight`,
|
|
6
|
+
* `codeIndexInFlight`. Each correctly stopped its OWN tick from overlapping
|
|
7
|
+
* itself, and none of them knew about the others, so a code-intelligence sweep
|
|
8
|
+
* could run at the same time as a full sync walk and an intent drain. All three
|
|
9
|
+
* are heavy: the sweep spawns codeindex and holds its output, the walk holds a
|
|
10
|
+
* session container, the drain copies files. Measured with a sweep and a walk
|
|
11
|
+
* overlapping, the main thread sat at 80–95% and RSS more than doubled.
|
|
12
|
+
*
|
|
13
|
+
* They also could not express what the daemon actually wants: shipping to the
|
|
14
|
+
* server matters more than refreshing code intelligence, so if both are due, the
|
|
15
|
+
* sweep should wait.
|
|
16
|
+
*
|
|
17
|
+
* ── Semantics ────────────────────────────────────────────────────────────
|
|
18
|
+
* SERIAL — exactly one task runs at a time.
|
|
19
|
+
*
|
|
20
|
+
* COALESCED PER KIND — if a kind is already waiting, a new request for that kind
|
|
21
|
+
* is dropped rather than queued twice. Both would do the same work against the
|
|
22
|
+
* same state, and the second would find nothing to do. This preserves what the
|
|
23
|
+
* booleans got right (no self-overlap) without the "silently skipped" behaviour:
|
|
24
|
+
* a request that arrives while its kind is RUNNING sets a re-run flag, so a file
|
|
25
|
+
* change during a walk still gets a walk afterwards instead of being lost.
|
|
26
|
+
*
|
|
27
|
+
* PRIORITISED — lower number first, and only among tasks already waiting. A
|
|
28
|
+
* running task is never preempted; a long sweep still finishes.
|
|
29
|
+
*/
|
|
30
|
+
export type TickKind = string;
|
|
31
|
+
export interface TickQueueOpts {
|
|
32
|
+
/** Reports every state change; the daemon logs through it. */
|
|
33
|
+
onError?: (kind: TickKind, err: unknown) => void;
|
|
34
|
+
}
|
|
35
|
+
export declare class TickQueue {
|
|
36
|
+
private readonly opts;
|
|
37
|
+
private waiting;
|
|
38
|
+
private running;
|
|
39
|
+
/** Kinds that asked to run while they were already running. */
|
|
40
|
+
private rerun;
|
|
41
|
+
constructor(opts?: TickQueueOpts);
|
|
42
|
+
/** The kind currently executing, or null when idle. Diagnostics only. */
|
|
43
|
+
get active(): TickKind | null;
|
|
44
|
+
get depth(): number;
|
|
45
|
+
/**
|
|
46
|
+
* Ask for `kind` to run. Returns immediately — this is a scheduler, not a
|
|
47
|
+
* `await`-able task.
|
|
48
|
+
*/
|
|
49
|
+
request(kind: TickKind, priority: number, run: () => Promise<void>): void;
|
|
50
|
+
private pump;
|
|
51
|
+
/** Tests: settle the queue. */
|
|
52
|
+
drain(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Priorities for the daemon's ticks. Lower runs first.
|
|
56
|
+
*
|
|
57
|
+
* Shipping to the server is the product; everything else is enrichment. Code
|
|
58
|
+
* intelligence is last because it is the heaviest and the least urgent — it
|
|
59
|
+
* refreshes on a 180-minute cadence, so waiting out a walk costs nothing.
|
|
60
|
+
*/
|
|
61
|
+
export declare const TICK_PRIORITY: {
|
|
62
|
+
readonly sync: 10;
|
|
63
|
+
readonly intents: 20;
|
|
64
|
+
readonly codeIndex: 30;
|
|
65
|
+
readonly housekeeping: 40;
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=tick-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tick-queue.d.ts","sourceRoot":"","sources":["../src/tick-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC;AAQ9B,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAClD;AAED,qBAAa,SAAS;IAMR,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,OAAO,CAAyB;IACxC,+DAA+D;IAC/D,OAAO,CAAC,KAAK,CAAuB;gBAEP,IAAI,GAAE,aAAkB;IAErD,yEAAyE;IACzE,IAAI,MAAM,IAAI,QAAQ,GAAG,IAAI,CAAyB;IACtD,IAAI,KAAK,IAAI,MAAM,CAAgC;IAEnD;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;YAY3D,IAAI;IAuBlB,+BAA+B;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAK7B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;CAKhB,CAAC"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One background task at a time, in priority order, with same-kind coalescing.
|
|
3
|
+
*
|
|
4
|
+
* ── What it replaces ─────────────────────────────────────────────────────
|
|
5
|
+
* Three independent in-flight booleans — `syncInFlight`, `intentDrainInFlight`,
|
|
6
|
+
* `codeIndexInFlight`. Each correctly stopped its OWN tick from overlapping
|
|
7
|
+
* itself, and none of them knew about the others, so a code-intelligence sweep
|
|
8
|
+
* could run at the same time as a full sync walk and an intent drain. All three
|
|
9
|
+
* are heavy: the sweep spawns codeindex and holds its output, the walk holds a
|
|
10
|
+
* session container, the drain copies files. Measured with a sweep and a walk
|
|
11
|
+
* overlapping, the main thread sat at 80–95% and RSS more than doubled.
|
|
12
|
+
*
|
|
13
|
+
* They also could not express what the daemon actually wants: shipping to the
|
|
14
|
+
* server matters more than refreshing code intelligence, so if both are due, the
|
|
15
|
+
* sweep should wait.
|
|
16
|
+
*
|
|
17
|
+
* ── Semantics ────────────────────────────────────────────────────────────
|
|
18
|
+
* SERIAL — exactly one task runs at a time.
|
|
19
|
+
*
|
|
20
|
+
* COALESCED PER KIND — if a kind is already waiting, a new request for that kind
|
|
21
|
+
* is dropped rather than queued twice. Both would do the same work against the
|
|
22
|
+
* same state, and the second would find nothing to do. This preserves what the
|
|
23
|
+
* booleans got right (no self-overlap) without the "silently skipped" behaviour:
|
|
24
|
+
* a request that arrives while its kind is RUNNING sets a re-run flag, so a file
|
|
25
|
+
* change during a walk still gets a walk afterwards instead of being lost.
|
|
26
|
+
*
|
|
27
|
+
* PRIORITISED — lower number first, and only among tasks already waiting. A
|
|
28
|
+
* running task is never preempted; a long sweep still finishes.
|
|
29
|
+
*/
|
|
30
|
+
export class TickQueue {
|
|
31
|
+
opts;
|
|
32
|
+
waiting = [];
|
|
33
|
+
running = null;
|
|
34
|
+
/** Kinds that asked to run while they were already running. */
|
|
35
|
+
rerun = new Set();
|
|
36
|
+
constructor(opts = {}) {
|
|
37
|
+
this.opts = opts;
|
|
38
|
+
}
|
|
39
|
+
/** The kind currently executing, or null when idle. Diagnostics only. */
|
|
40
|
+
get active() { return this.running; }
|
|
41
|
+
get depth() { return this.waiting.length; }
|
|
42
|
+
/**
|
|
43
|
+
* Ask for `kind` to run. Returns immediately — this is a scheduler, not a
|
|
44
|
+
* `await`-able task.
|
|
45
|
+
*/
|
|
46
|
+
request(kind, priority, run) {
|
|
47
|
+
if (this.running === kind) {
|
|
48
|
+
// Its state changed underneath it; run once more when it finishes rather
|
|
49
|
+
// than dropping the request (a file change during a walk must not be lost).
|
|
50
|
+
this.rerun.add(kind);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (this.waiting.some((w) => w.kind === kind))
|
|
54
|
+
return; // coalesced
|
|
55
|
+
this.waiting.push({ kind, priority, run });
|
|
56
|
+
void this.pump();
|
|
57
|
+
}
|
|
58
|
+
async pump() {
|
|
59
|
+
if (this.running !== null)
|
|
60
|
+
return;
|
|
61
|
+
const next = this.waiting
|
|
62
|
+
.map((w, i) => ({ w, i }))
|
|
63
|
+
.sort((a, b) => (a.w.priority - b.w.priority) || (a.i - b.i))[0];
|
|
64
|
+
if (!next)
|
|
65
|
+
return;
|
|
66
|
+
this.waiting.splice(next.i, 1);
|
|
67
|
+
const { kind, priority, run } = next.w;
|
|
68
|
+
this.running = kind;
|
|
69
|
+
try {
|
|
70
|
+
await run();
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
this.opts.onError?.(kind, err);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
this.running = null;
|
|
77
|
+
if (this.rerun.delete(kind)) {
|
|
78
|
+
// Re-queue at its own priority; it takes its turn like anything else.
|
|
79
|
+
this.waiting.push({ kind, priority, run });
|
|
80
|
+
}
|
|
81
|
+
void this.pump();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Tests: settle the queue. */
|
|
85
|
+
async drain() {
|
|
86
|
+
while (this.running !== null || this.waiting.length > 0) {
|
|
87
|
+
await new Promise((r) => setImmediate(r));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Priorities for the daemon's ticks. Lower runs first.
|
|
93
|
+
*
|
|
94
|
+
* Shipping to the server is the product; everything else is enrichment. Code
|
|
95
|
+
* intelligence is last because it is the heaviest and the least urgent — it
|
|
96
|
+
* refreshes on a 180-minute cadence, so waiting out a walk costs nothing.
|
|
97
|
+
*/
|
|
98
|
+
export const TICK_PRIORITY = {
|
|
99
|
+
sync: 10,
|
|
100
|
+
intents: 20,
|
|
101
|
+
codeIndex: 30,
|
|
102
|
+
housekeeping: 40,
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=tick-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tick-queue.js","sourceRoot":"","sources":["../src/tick-queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse to ship a transcript over a connection anyone can read.
|
|
3
|
+
*
|
|
4
|
+
* ── The hole this closes ─────────────────────────────────────────────────
|
|
5
|
+
* TLS to the hosted service was never in doubt — valid cert, HSTS for a year,
|
|
6
|
+
* plain HTTP redirected. But NOTHING in the collector required it. A user who
|
|
7
|
+
* typed `chat-recall login http://recall.mycompany.com` got exactly what they
|
|
8
|
+
* asked for: every transcript on the machine, in cleartext, over the open
|
|
9
|
+
* internet, with no warning at any point. `isLocalHost()` existed to relax
|
|
10
|
+
* upload pacing for LAN targets, not to gate the scheme, so a public host over
|
|
11
|
+
* plain HTTP passed every check.
|
|
12
|
+
*
|
|
13
|
+
* The payload makes this worse than a generic cleartext warning deserves. A sync
|
|
14
|
+
* body carries whole transcripts: source code, file paths, and — despite
|
|
15
|
+
* client-side redaction being good — whatever the detectors did not recognise as
|
|
16
|
+
* a secret. This is the single highest-value thing the product ever puts on a
|
|
17
|
+
* wire.
|
|
18
|
+
*
|
|
19
|
+
* ── Where the line is ────────────────────────────────────────────────────
|
|
20
|
+
* Plain HTTP is fine to a host only YOU can reach, and that is the whole
|
|
21
|
+
* exception. Loopback, mDNS/`.lan` names, RFC1918, IPv6 loopback and ULA, and
|
|
22
|
+
* the CGNAT range Tailscale hands out — a self-hoster on any of those has no
|
|
23
|
+
* public path to intercept. Everything else needs TLS.
|
|
24
|
+
*
|
|
25
|
+
* `CHAT_RECALL_ALLOW_INSECURE_HTTP=1` overrides it, for the real case this
|
|
26
|
+
* would otherwise break: a corporate host reached through a VPN whose name and
|
|
27
|
+
* address both look public. That is a deliberate, per-machine decision, which is
|
|
28
|
+
* exactly the bar an override like this should meet.
|
|
29
|
+
*/
|
|
30
|
+
/** Hosts only reachable from the user's own machine or network. */
|
|
31
|
+
export declare function isPrivateHost(serverUrl: string): boolean;
|
|
32
|
+
/** Explicit, per-machine opt-out for a VPN-reached host that looks public. */
|
|
33
|
+
export declare function insecureHttpAllowed(env?: NodeJS.ProcessEnv): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Why this target must not be used, or null when it is safe.
|
|
36
|
+
*
|
|
37
|
+
* Pure, so both the login gate and the sync-time gate can share one rule and a
|
|
38
|
+
* test can pin it without a network.
|
|
39
|
+
*/
|
|
40
|
+
export declare function transportRisk(serverUrl: string, env?: NodeJS.ProcessEnv): string | null;
|
|
41
|
+
/** Throwing form, for the login path. */
|
|
42
|
+
export declare function assertTransportSafe(serverUrl: string, env?: NodeJS.ProcessEnv): void;
|
|
43
|
+
//# sourceMappingURL=transport-safety.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport-safety.d.ts","sourceRoot":"","sources":["../src/transport-safety.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,mEAAmE;AACnE,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAgBxD;AAED,8EAA8E;AAC9E,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAGjF;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,GAAG,IAAI,CAWf;AAED,yCAAyC;AACzC,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,IAAI,CAGjG"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refuse to ship a transcript over a connection anyone can read.
|
|
3
|
+
*
|
|
4
|
+
* ── The hole this closes ─────────────────────────────────────────────────
|
|
5
|
+
* TLS to the hosted service was never in doubt — valid cert, HSTS for a year,
|
|
6
|
+
* plain HTTP redirected. But NOTHING in the collector required it. A user who
|
|
7
|
+
* typed `chat-recall login http://recall.mycompany.com` got exactly what they
|
|
8
|
+
* asked for: every transcript on the machine, in cleartext, over the open
|
|
9
|
+
* internet, with no warning at any point. `isLocalHost()` existed to relax
|
|
10
|
+
* upload pacing for LAN targets, not to gate the scheme, so a public host over
|
|
11
|
+
* plain HTTP passed every check.
|
|
12
|
+
*
|
|
13
|
+
* The payload makes this worse than a generic cleartext warning deserves. A sync
|
|
14
|
+
* body carries whole transcripts: source code, file paths, and — despite
|
|
15
|
+
* client-side redaction being good — whatever the detectors did not recognise as
|
|
16
|
+
* a secret. This is the single highest-value thing the product ever puts on a
|
|
17
|
+
* wire.
|
|
18
|
+
*
|
|
19
|
+
* ── Where the line is ────────────────────────────────────────────────────
|
|
20
|
+
* Plain HTTP is fine to a host only YOU can reach, and that is the whole
|
|
21
|
+
* exception. Loopback, mDNS/`.lan` names, RFC1918, IPv6 loopback and ULA, and
|
|
22
|
+
* the CGNAT range Tailscale hands out — a self-hoster on any of those has no
|
|
23
|
+
* public path to intercept. Everything else needs TLS.
|
|
24
|
+
*
|
|
25
|
+
* `CHAT_RECALL_ALLOW_INSECURE_HTTP=1` overrides it, for the real case this
|
|
26
|
+
* would otherwise break: a corporate host reached through a VPN whose name and
|
|
27
|
+
* address both look public. That is a deliberate, per-machine decision, which is
|
|
28
|
+
* exactly the bar an override like this should meet.
|
|
29
|
+
*/
|
|
30
|
+
/** Hosts only reachable from the user's own machine or network. */
|
|
31
|
+
export function isPrivateHost(serverUrl) {
|
|
32
|
+
let host = '';
|
|
33
|
+
try {
|
|
34
|
+
host = new URL(serverUrl).hostname.toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
// IPv6 arrives bracket-stripped from URL.hostname.
|
|
40
|
+
if (host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '0.0.0.0')
|
|
41
|
+
return true;
|
|
42
|
+
if (host.endsWith('.local') || host.endsWith('.lan') || host.endsWith('.internal'))
|
|
43
|
+
return true;
|
|
44
|
+
if (/^10\./.test(host))
|
|
45
|
+
return true; // 10.0.0.0/8
|
|
46
|
+
if (/^192\.168\./.test(host))
|
|
47
|
+
return true; // 192.168.0.0/16
|
|
48
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(host))
|
|
49
|
+
return true; // 172.16.0.0/12
|
|
50
|
+
if (/^127\./.test(host))
|
|
51
|
+
return true; // loopback/8
|
|
52
|
+
// 100.64.0.0/10 — CGNAT, and what Tailscale assigns. A self-hoster reaching
|
|
53
|
+
// their box over a tailnet is on a private path by construction.
|
|
54
|
+
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(host))
|
|
55
|
+
return true;
|
|
56
|
+
if (/^f[cd][0-9a-f]{2}:/.test(host))
|
|
57
|
+
return true; // IPv6 ULA fc00::/7
|
|
58
|
+
if (/^fe80:/.test(host))
|
|
59
|
+
return true; // IPv6 link-local
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/** Explicit, per-machine opt-out for a VPN-reached host that looks public. */
|
|
63
|
+
export function insecureHttpAllowed(env = process.env) {
|
|
64
|
+
const v = (env.CHAT_RECALL_ALLOW_INSECURE_HTTP || '').trim().toLowerCase();
|
|
65
|
+
return v === '1' || v === 'true' || v === 'yes';
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Why this target must not be used, or null when it is safe.
|
|
69
|
+
*
|
|
70
|
+
* Pure, so both the login gate and the sync-time gate can share one rule and a
|
|
71
|
+
* test can pin it without a network.
|
|
72
|
+
*/
|
|
73
|
+
export function transportRisk(serverUrl, env = process.env) {
|
|
74
|
+
let url;
|
|
75
|
+
try {
|
|
76
|
+
url = new URL(serverUrl);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return `not a valid server URL: ${serverUrl}`;
|
|
80
|
+
}
|
|
81
|
+
if (url.protocol === 'https:')
|
|
82
|
+
return null;
|
|
83
|
+
if (url.protocol !== 'http:')
|
|
84
|
+
return `unsupported scheme '${url.protocol}' — use https://`;
|
|
85
|
+
if (isPrivateHost(serverUrl))
|
|
86
|
+
return null; // your own network
|
|
87
|
+
if (insecureHttpAllowed(env))
|
|
88
|
+
return null; // deliberate override
|
|
89
|
+
return `refusing to sync to ${url.host} over plain HTTP — your transcripts, `
|
|
90
|
+
+ 'file paths and any secret the redactor did not catch would cross the '
|
|
91
|
+
+ `internet in cleartext. Use https://${url.host} instead. If this host is `
|
|
92
|
+
+ 'only reachable through a VPN, set CHAT_RECALL_ALLOW_INSECURE_HTTP=1.';
|
|
93
|
+
}
|
|
94
|
+
/** Throwing form, for the login path. */
|
|
95
|
+
export function assertTransportSafe(serverUrl, env = process.env) {
|
|
96
|
+
const risk = transportRisk(serverUrl, env);
|
|
97
|
+
if (risk)
|
|
98
|
+
throw new Error(risk);
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=transport-safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport-safety.js","sourceRoot":"","sources":["../src/transport-safety.ts"],"names":[],"mappings":"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"}
|
package/dist/verify-repair.js
CHANGED
|
@@ -35,7 +35,7 @@ export function forceFullResync(server, sessionIds) {
|
|
|
35
35
|
let cleared = 0;
|
|
36
36
|
for (const key of Object.keys(data)) {
|
|
37
37
|
// Ledger keys are prefixed ids for non-Claude tools; match either form.
|
|
38
|
-
const bare = key.replace(/^(opencode_|gemini_|codex_|agy_)/, '');
|
|
38
|
+
const bare = key.replace(/^(opencode_|gemini_|codex_|agy_|cursor_)/, '');
|
|
39
39
|
if (!wanted.has(key) && !wanted.has(bare))
|
|
40
40
|
continue;
|
|
41
41
|
const row = data[key];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verify-repair.js","sourceRoot":"","sources":["../src/verify-repair.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEpF;qEACqE;AACrE,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,UAAoB;IAClE,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAEtC,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9B,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC9E,IAAI,CAAC;YAAC,YAAY,CAAC,IAAI,EAAE,GAAG,IAAI,QAAQ,KAAK,EAAE,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,wBAAwB,CAAC,CAAC;IACxF,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACnC,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,wEAAwE;QACxE,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"verify-repair.js","sourceRoot":"","sources":["../src/verify-repair.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEpF;qEACqE;AACrE,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,UAAoB;IAClE,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAEtC,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9B,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC9E,IAAI,CAAC;YAAC,YAAY,CAAC,IAAI,EAAE,GAAG,IAAI,QAAQ,KAAK,EAAE,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,wBAAwB,CAAC,CAAC;IACxF,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IACnC,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,wEAAwE;QACxE,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,0CAA0C,EAAE,EAAE,CAAC,CAAC;QACzE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QACpD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QACtB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;YAAC,OAAO,EAAE,CAAC;YAAC,SAAS;QAAC,CAAC;QACvF,MAAM,CAAC,GAAG,GAAyC,CAAC;QACpD,OAAO,CAAC,CAAC,CAAC,CAAC,CAAM,cAAc;QAC/B,OAAO,CAAC,CAAC,CAAC,CAAC,CAAM,sBAAsB;QACvC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAM,4DAA4D;QAC7E,OAAO,CAAC,CAAC,CAAC,CAAC,CAAM,sDAAsD;QACvE,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,OAAO,GAAG,CAAC;QAAE,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACjD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|