@cleocode/core 2026.4.47 → 2026.4.49
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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +730 -362
- package/dist/index.js.map +4 -4
- package/dist/internal.d.ts +4 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1061 -683
- package/dist/internal.js.map +4 -4
- package/dist/nexus/index.d.ts +1 -1
- package/dist/nexus/index.d.ts.map +1 -1
- package/dist/nexus/registry.d.ts +25 -0
- package/dist/nexus/registry.d.ts.map +1 -1
- package/dist/store/nexus-schema.d.ts +64 -0
- package/dist/store/nexus-schema.d.ts.map +1 -1
- package/dist/store/nexus-validation-schemas.d.ts +128 -0
- package/dist/store/nexus-validation-schemas.d.ts.map +1 -1
- package/dist/telemetry/index.d.ts +107 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/schema.d.ts +228 -0
- package/dist/telemetry/schema.d.ts.map +1 -0
- package/dist/telemetry/sqlite.d.ts +33 -0
- package/dist/telemetry/sqlite.d.ts.map +1 -0
- package/migrations/drizzle-telemetry/20260415000001_t624-initial/migration.sql +23 -0
- package/migrations/drizzle-telemetry/20260415000001_t624-initial/snapshot.json +35 -0
- package/package.json +8 -8
- package/src/__tests__/telemetry.test.ts +221 -0
- package/src/index.ts +1 -0
- package/src/internal.ts +20 -1
- package/src/nexus/index.ts +2 -0
- package/src/nexus/registry.ts +103 -1
- package/src/store/nexus-schema.ts +9 -0
- package/src/telemetry/index.ts +341 -0
- package/src/telemetry/schema.ts +68 -0
- package/src/telemetry/sqlite.ts +140 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite store for telemetry.db via drizzle-orm/node-sqlite + node:sqlite.
|
|
3
|
+
*
|
|
4
|
+
* Stores opt-in command telemetry in ~/.local/share/cleo/telemetry.db.
|
|
5
|
+
* Follows the same singleton + WAL + migration pattern as brain-sqlite.ts.
|
|
6
|
+
* Telemetry is disabled by default — check isTelemetryEnabled() before writing.
|
|
7
|
+
*
|
|
8
|
+
* @task T624
|
|
9
|
+
*/
|
|
10
|
+
import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite';
|
|
11
|
+
import * as telemetrySchema from './schema.js';
|
|
12
|
+
/** Schema version. Single source of truth. */
|
|
13
|
+
export declare const TELEMETRY_SCHEMA_VERSION = "1.0.0";
|
|
14
|
+
/**
|
|
15
|
+
* Get the absolute path to telemetry.db in the global CLEO home directory.
|
|
16
|
+
* Linux: ~/.local/share/cleo/telemetry.db
|
|
17
|
+
*/
|
|
18
|
+
export declare function getTelemetryDbPath(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the drizzle-telemetry migrations folder.
|
|
21
|
+
* Handles both src/ (dev via tsx) and dist/ (bundled) layouts.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveTelemetryMigrationsFolder(): string;
|
|
24
|
+
/**
|
|
25
|
+
* Reset the singleton (used in tests).
|
|
26
|
+
*/
|
|
27
|
+
export declare function resetTelemetryDbState(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Initialize telemetry.db (lazy singleton).
|
|
30
|
+
* Creates the file and runs migrations on first call.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getTelemetryDb(): Promise<NodeSQLiteDatabase<typeof telemetrySchema>>;
|
|
33
|
+
//# sourceMappingURL=sqlite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/telemetry/sqlite.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAKlE,OAAO,KAAK,eAAe,MAAM,aAAa,CAAC;AAK/C,8CAA8C;AAC9C,eAAO,MAAM,wBAAwB,UAAU,CAAC;AAQhD;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED;;;GAGG;AACH,wBAAgB,gCAAgC,IAAI,MAAM,CAQzD;AAgCD;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAU5C;AAED;;;GAGG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,kBAAkB,CAAC,OAAO,eAAe,CAAC,CAAC,CAmC1F"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS `telemetry_events` (
|
|
2
|
+
`id` text PRIMARY KEY,
|
|
3
|
+
`anonymous_id` text NOT NULL,
|
|
4
|
+
`domain` text NOT NULL,
|
|
5
|
+
`gateway` text NOT NULL,
|
|
6
|
+
`operation` text NOT NULL,
|
|
7
|
+
`command` text NOT NULL,
|
|
8
|
+
`exit_code` integer NOT NULL DEFAULT 0,
|
|
9
|
+
`duration_ms` integer NOT NULL,
|
|
10
|
+
`error_code` text,
|
|
11
|
+
`timestamp` text NOT NULL DEFAULT (datetime('now'))
|
|
12
|
+
);
|
|
13
|
+
--> statement-breakpoint
|
|
14
|
+
CREATE TABLE IF NOT EXISTS `telemetry_schema_meta` (
|
|
15
|
+
`key` text PRIMARY KEY,
|
|
16
|
+
`value` text NOT NULL
|
|
17
|
+
);
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
CREATE INDEX IF NOT EXISTS `idx_telemetry_command` ON `telemetry_events` (`command`);--> statement-breakpoint
|
|
20
|
+
CREATE INDEX IF NOT EXISTS `idx_telemetry_domain` ON `telemetry_events` (`domain`);--> statement-breakpoint
|
|
21
|
+
CREATE INDEX IF NOT EXISTS `idx_telemetry_exit_code` ON `telemetry_events` (`exit_code`);--> statement-breakpoint
|
|
22
|
+
CREATE INDEX IF NOT EXISTS `idx_telemetry_timestamp` ON `telemetry_events` (`timestamp`);--> statement-breakpoint
|
|
23
|
+
CREATE INDEX IF NOT EXISTS `idx_telemetry_duration` ON `telemetry_events` (`duration_ms`);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "7",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"id": "a1b2c3d4-0000-0000-0000-t624telemetry",
|
|
5
|
+
"prevIds": [
|
|
6
|
+
"00000000-0000-0000-0000-000000000000"
|
|
7
|
+
],
|
|
8
|
+
"ddl": [
|
|
9
|
+
{
|
|
10
|
+
"name": "telemetry_events",
|
|
11
|
+
"columns": {
|
|
12
|
+
"id": { "name": "id", "type": "text", "notNull": true, "primaryKey": true },
|
|
13
|
+
"anonymous_id": { "name": "anonymous_id", "type": "text", "notNull": true },
|
|
14
|
+
"domain": { "name": "domain", "type": "text", "notNull": true },
|
|
15
|
+
"gateway": { "name": "gateway", "type": "text", "notNull": true },
|
|
16
|
+
"operation": { "name": "operation", "type": "text", "notNull": true },
|
|
17
|
+
"command": { "name": "command", "type": "text", "notNull": true },
|
|
18
|
+
"exit_code": { "name": "exit_code", "type": "integer", "notNull": true, "default": 0 },
|
|
19
|
+
"duration_ms": { "name": "duration_ms", "type": "integer", "notNull": true },
|
|
20
|
+
"error_code": { "name": "error_code", "type": "text", "notNull": false },
|
|
21
|
+
"timestamp": { "name": "timestamp", "type": "text", "notNull": true }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "telemetry_schema_meta",
|
|
26
|
+
"columns": {
|
|
27
|
+
"key": { "name": "key", "type": "text", "notNull": true, "primaryKey": true },
|
|
28
|
+
"value": { "name": "value", "type": "text", "notNull": true }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"indexes": {},
|
|
33
|
+
"foreignKeys": {},
|
|
34
|
+
"tables": {}
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/core",
|
|
3
|
-
"version": "2026.4.
|
|
3
|
+
"version": "2026.4.49",
|
|
4
4
|
"description": "CLEO core business logic kernel — tasks, sessions, memory, orchestration, lifecycle, with bundled SQLite store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -63,13 +63,13 @@
|
|
|
63
63
|
"write-file-atomic": "^7.0.1",
|
|
64
64
|
"yaml": "^2.8.3",
|
|
65
65
|
"zod": "^4.3.6",
|
|
66
|
-
"@cleocode/adapters": "2026.4.
|
|
67
|
-
"@cleocode/agents": "2026.4.
|
|
68
|
-
"@cleocode/caamp": "2026.4.
|
|
69
|
-
"@cleocode/
|
|
70
|
-
"@cleocode/
|
|
71
|
-
"@cleocode/nexus": "2026.4.
|
|
72
|
-
"@cleocode/skills": "2026.4.
|
|
66
|
+
"@cleocode/adapters": "2026.4.49",
|
|
67
|
+
"@cleocode/agents": "2026.4.49",
|
|
68
|
+
"@cleocode/caamp": "2026.4.49",
|
|
69
|
+
"@cleocode/contracts": "2026.4.49",
|
|
70
|
+
"@cleocode/lafs": "2026.4.49",
|
|
71
|
+
"@cleocode/nexus": "2026.4.49",
|
|
72
|
+
"@cleocode/skills": "2026.4.49"
|
|
73
73
|
},
|
|
74
74
|
"engines": {
|
|
75
75
|
"node": ">=24.0.0"
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the telemetry module (T624 — diagnostic feedback loop).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Opt-in / opt-out config management
|
|
6
|
+
* - Anonymous ID generation (stable across calls, new on first enable)
|
|
7
|
+
* - recordTelemetryEvent is a no-op when disabled
|
|
8
|
+
* - buildDiagnosticsReport returns null when disabled
|
|
9
|
+
* - buildDiagnosticsReport returns correct aggregates when enabled
|
|
10
|
+
*
|
|
11
|
+
* @task T624
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
18
|
+
import {
|
|
19
|
+
buildDiagnosticsReport,
|
|
20
|
+
disableTelemetry,
|
|
21
|
+
enableTelemetry,
|
|
22
|
+
isTelemetryEnabled,
|
|
23
|
+
loadTelemetryConfig,
|
|
24
|
+
recordTelemetryEvent,
|
|
25
|
+
} from '../telemetry/index.js';
|
|
26
|
+
import { resetTelemetryDbState } from '../telemetry/sqlite.js';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Test setup: redirect CLEO_HOME to a temp directory for isolation
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
let tempDir: string;
|
|
33
|
+
const origCleoHome = process.env['CLEO_HOME'];
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
tempDir = await mkdtemp(join(tmpdir(), 'cleo-telemetry-test-'));
|
|
37
|
+
process.env['CLEO_HOME'] = tempDir;
|
|
38
|
+
// Reset the DB singleton so each test starts clean
|
|
39
|
+
resetTelemetryDbState();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
resetTelemetryDbState();
|
|
44
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
45
|
+
if (origCleoHome !== undefined) {
|
|
46
|
+
process.env['CLEO_HOME'] = origCleoHome;
|
|
47
|
+
} else {
|
|
48
|
+
delete process.env['CLEO_HOME'];
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Config management
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
describe('loadTelemetryConfig', () => {
|
|
57
|
+
it('returns disabled config when file does not exist', () => {
|
|
58
|
+
const config = loadTelemetryConfig();
|
|
59
|
+
expect(config.enabled).toBe(false);
|
|
60
|
+
expect(config.anonymousId).toBe('');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('enableTelemetry', () => {
|
|
65
|
+
it('sets enabled=true and generates a non-empty anonymousId', () => {
|
|
66
|
+
const config = enableTelemetry();
|
|
67
|
+
expect(config.enabled).toBe(true);
|
|
68
|
+
expect(config.anonymousId).toMatch(
|
|
69
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('preserves existing anonymousId on subsequent calls', () => {
|
|
74
|
+
const first = enableTelemetry();
|
|
75
|
+
const second = enableTelemetry();
|
|
76
|
+
expect(second.anonymousId).toBe(first.anonymousId);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('disableTelemetry', () => {
|
|
81
|
+
it('sets enabled=false', () => {
|
|
82
|
+
enableTelemetry();
|
|
83
|
+
const config = disableTelemetry();
|
|
84
|
+
expect(config.enabled).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('preserves anonymousId after disable', () => {
|
|
88
|
+
const { anonymousId } = enableTelemetry();
|
|
89
|
+
const after = disableTelemetry();
|
|
90
|
+
expect(after.anonymousId).toBe(anonymousId);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('isTelemetryEnabled', () => {
|
|
95
|
+
it('returns false by default', () => {
|
|
96
|
+
expect(isTelemetryEnabled()).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns true after enable', () => {
|
|
100
|
+
enableTelemetry();
|
|
101
|
+
expect(isTelemetryEnabled()).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns false after disable', () => {
|
|
105
|
+
enableTelemetry();
|
|
106
|
+
disableTelemetry();
|
|
107
|
+
expect(isTelemetryEnabled()).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Event recording
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
describe('recordTelemetryEvent', () => {
|
|
116
|
+
it('is a no-op when telemetry is disabled (no error thrown)', async () => {
|
|
117
|
+
// Ensure disabled
|
|
118
|
+
disableTelemetry();
|
|
119
|
+
await expect(
|
|
120
|
+
recordTelemetryEvent({
|
|
121
|
+
domain: 'tasks',
|
|
122
|
+
gateway: 'query',
|
|
123
|
+
operation: 'show',
|
|
124
|
+
durationMs: 42,
|
|
125
|
+
exitCode: 0,
|
|
126
|
+
}),
|
|
127
|
+
).resolves.toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('writes an event to the DB when enabled', async () => {
|
|
131
|
+
enableTelemetry();
|
|
132
|
+
await recordTelemetryEvent({
|
|
133
|
+
domain: 'tasks',
|
|
134
|
+
gateway: 'mutate',
|
|
135
|
+
operation: 'add',
|
|
136
|
+
durationMs: 100,
|
|
137
|
+
exitCode: 0,
|
|
138
|
+
});
|
|
139
|
+
// Verify by building a report
|
|
140
|
+
const report = await buildDiagnosticsReport(1);
|
|
141
|
+
expect(report).not.toBeNull();
|
|
142
|
+
expect(report!.totalEvents).toBeGreaterThanOrEqual(1);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Diagnostics report
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
describe('buildDiagnosticsReport', () => {
|
|
151
|
+
it('returns null when telemetry is disabled', async () => {
|
|
152
|
+
disableTelemetry();
|
|
153
|
+
const report = await buildDiagnosticsReport(30);
|
|
154
|
+
expect(report).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('returns a report with zero events when DB is empty', async () => {
|
|
158
|
+
enableTelemetry();
|
|
159
|
+
const report = await buildDiagnosticsReport(30);
|
|
160
|
+
expect(report).not.toBeNull();
|
|
161
|
+
expect(report!.totalEvents).toBe(0);
|
|
162
|
+
expect(report!.topFailing).toHaveLength(0);
|
|
163
|
+
expect(report!.topSlow).toHaveLength(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('surfaces high-failure-rate commands in topFailing', async () => {
|
|
167
|
+
enableTelemetry();
|
|
168
|
+
|
|
169
|
+
// Record 10 failures and 2 successes for tasks.add
|
|
170
|
+
for (let i = 0; i < 10; i++) {
|
|
171
|
+
await recordTelemetryEvent({
|
|
172
|
+
domain: 'tasks',
|
|
173
|
+
gateway: 'mutate',
|
|
174
|
+
operation: 'add',
|
|
175
|
+
durationMs: 50,
|
|
176
|
+
exitCode: 6,
|
|
177
|
+
errorCode: 'E_VALIDATION',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
for (let i = 0; i < 2; i++) {
|
|
181
|
+
await recordTelemetryEvent({
|
|
182
|
+
domain: 'tasks',
|
|
183
|
+
gateway: 'mutate',
|
|
184
|
+
operation: 'add',
|
|
185
|
+
durationMs: 50,
|
|
186
|
+
exitCode: 0,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const report = await buildDiagnosticsReport(1);
|
|
191
|
+
expect(report).not.toBeNull();
|
|
192
|
+
const failing = report!.topFailing.find((c) => c.command === 'tasks.add');
|
|
193
|
+
expect(failing).toBeDefined();
|
|
194
|
+
expect(failing!.failureCount).toBe(10);
|
|
195
|
+
expect(failing!.failureRate).toBeCloseTo(10 / 12);
|
|
196
|
+
expect(failing!.topErrorCode).toBe('E_VALIDATION');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('generates BRAIN observation text for failing commands', async () => {
|
|
200
|
+
enableTelemetry();
|
|
201
|
+
|
|
202
|
+
// Need at least 5 invocations to appear in topFailing
|
|
203
|
+
for (let i = 0; i < 8; i++) {
|
|
204
|
+
await recordTelemetryEvent({
|
|
205
|
+
domain: 'session',
|
|
206
|
+
gateway: 'mutate',
|
|
207
|
+
operation: 'start',
|
|
208
|
+
durationMs: 200,
|
|
209
|
+
exitCode: i < 7 ? 1 : 0,
|
|
210
|
+
errorCode: i < 7 ? 'E_GENERAL' : null,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const report = await buildDiagnosticsReport(1);
|
|
215
|
+
expect(report).not.toBeNull();
|
|
216
|
+
expect(report!.observations.length).toBeGreaterThan(0);
|
|
217
|
+
const obs = report!.observations[0]!;
|
|
218
|
+
expect(obs).toContain('session.start');
|
|
219
|
+
expect(obs).toContain('%');
|
|
220
|
+
});
|
|
221
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -72,6 +72,7 @@ export * as sticky from './sticky/index.js';
|
|
|
72
72
|
export * as system from './system/index.js';
|
|
73
73
|
export * as taskWork from './task-work/index.js';
|
|
74
74
|
export * as tasks from './tasks/index.js';
|
|
75
|
+
export * as telemetry from './telemetry/index.js';
|
|
75
76
|
export * as templates from './templates/index.js';
|
|
76
77
|
export * as ui from './ui/index.js';
|
|
77
78
|
export * as validation from './validation/index.js';
|
package/src/internal.ts
CHANGED
|
@@ -291,7 +291,7 @@ export {
|
|
|
291
291
|
export { searchAcrossProjects } from './nexus/discover.js';
|
|
292
292
|
export { setPermission } from './nexus/permissions.js';
|
|
293
293
|
export { resolveTask, validateSyntax } from './nexus/query.js';
|
|
294
|
-
export type { NexusPermissionLevel } from './nexus/registry.js';
|
|
294
|
+
export type { NexusPermissionLevel, NexusProject, NexusProjectStats } from './nexus/registry.js';
|
|
295
295
|
export {
|
|
296
296
|
nexusGetProject,
|
|
297
297
|
nexusInit,
|
|
@@ -301,6 +301,7 @@ export {
|
|
|
301
301
|
nexusSync,
|
|
302
302
|
nexusSyncAll,
|
|
303
303
|
nexusUnregister,
|
|
304
|
+
nexusUpdateIndexStats,
|
|
304
305
|
} from './nexus/registry.js';
|
|
305
306
|
export { getSharingStatus } from './nexus/sharing/index.js';
|
|
306
307
|
// Context
|
|
@@ -625,6 +626,24 @@ export {
|
|
|
625
626
|
coreTaskTree,
|
|
626
627
|
coreTaskUnarchive,
|
|
627
628
|
} from './tasks/task-ops.js';
|
|
629
|
+
// Self-improvement telemetry (T624)
|
|
630
|
+
export type {
|
|
631
|
+
CommandStats as TelemetryCommandStats,
|
|
632
|
+
DiagnosticsReport as TelemetryDiagnosticsReport,
|
|
633
|
+
TelemetryConfig,
|
|
634
|
+
TelemetryEvent,
|
|
635
|
+
} from './telemetry/index.js';
|
|
636
|
+
export {
|
|
637
|
+
buildDiagnosticsReport,
|
|
638
|
+
disableTelemetry,
|
|
639
|
+
enableTelemetry,
|
|
640
|
+
exportTelemetryEvents,
|
|
641
|
+
getTelemetryConfigPath,
|
|
642
|
+
getTelemetryDbPath,
|
|
643
|
+
isTelemetryEnabled,
|
|
644
|
+
loadTelemetryConfig,
|
|
645
|
+
recordTelemetryEvent,
|
|
646
|
+
} from './telemetry/index.js';
|
|
628
647
|
export type { IssueTemplate, TemplateConfig, TemplateSection } from './templates/parser.js';
|
|
629
648
|
// Templates
|
|
630
649
|
export {
|
package/src/nexus/index.ts
CHANGED
|
@@ -74,6 +74,7 @@ export {
|
|
|
74
74
|
type NexusPermissionLevel,
|
|
75
75
|
// Types
|
|
76
76
|
type NexusProject,
|
|
77
|
+
type NexusProjectStats,
|
|
77
78
|
type NexusRegistryFile,
|
|
78
79
|
nexusGetProject,
|
|
79
80
|
nexusInit,
|
|
@@ -85,6 +86,7 @@ export {
|
|
|
85
86
|
nexusSync,
|
|
86
87
|
nexusSyncAll,
|
|
87
88
|
nexusUnregister,
|
|
89
|
+
nexusUpdateIndexStats,
|
|
88
90
|
// Operations
|
|
89
91
|
readRegistry,
|
|
90
92
|
readRegistryRequired,
|
package/src/nexus/registry.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
15
|
import { mkdir } from 'node:fs/promises';
|
|
16
|
-
import { basename, join } from 'node:path';
|
|
16
|
+
import { basename, join, resolve } from 'node:path';
|
|
17
17
|
import { ExitCode } from '@cleocode/contracts';
|
|
18
18
|
import { CleoError } from '../errors.js';
|
|
19
19
|
import { getLogger } from '../logger.js';
|
|
@@ -36,6 +36,13 @@ export type NexusPermissionLevel = 'read' | 'write' | 'execute';
|
|
|
36
36
|
|
|
37
37
|
export type NexusHealthStatus = 'unknown' | 'healthy' | 'degraded' | 'unreachable';
|
|
38
38
|
|
|
39
|
+
/** Per-project code intelligence statistics stored in stats_json. */
|
|
40
|
+
export interface NexusProjectStats {
|
|
41
|
+
nodeCount: number;
|
|
42
|
+
relationCount: number;
|
|
43
|
+
fileCount: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
/** Domain representation of a registered Nexus project. */
|
|
40
47
|
export interface NexusProject {
|
|
41
48
|
hash: string;
|
|
@@ -50,6 +57,14 @@ export interface NexusProject {
|
|
|
50
57
|
lastSync: string;
|
|
51
58
|
taskCount: number;
|
|
52
59
|
labels: string[];
|
|
60
|
+
/** Absolute path to the project's brain.db. Null if not yet populated. */
|
|
61
|
+
brainDbPath: string | null;
|
|
62
|
+
/** Absolute path to the project's tasks.db. Null if not yet populated. */
|
|
63
|
+
tasksDbPath: string | null;
|
|
64
|
+
/** ISO 8601 timestamp of the last code intelligence index run. Null if never indexed. */
|
|
65
|
+
lastIndexed: string | null;
|
|
66
|
+
/** Code intelligence stats from the last index run. */
|
|
67
|
+
stats: NexusProjectStats;
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
/** Legacy registry file shape (pre-SQLite). Retained for migration compatibility. */
|
|
@@ -90,6 +105,17 @@ function rowToProject(row: ProjectRegistryRow): NexusProject {
|
|
|
90
105
|
} catch {
|
|
91
106
|
labels = [];
|
|
92
107
|
}
|
|
108
|
+
let stats: NexusProjectStats = { nodeCount: 0, relationCount: 0, fileCount: 0 };
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(row.statsJson ?? '{}') as Partial<NexusProjectStats>;
|
|
111
|
+
stats = {
|
|
112
|
+
nodeCount: parsed.nodeCount ?? 0,
|
|
113
|
+
relationCount: parsed.relationCount ?? 0,
|
|
114
|
+
fileCount: parsed.fileCount ?? 0,
|
|
115
|
+
};
|
|
116
|
+
} catch {
|
|
117
|
+
stats = { nodeCount: 0, relationCount: 0, fileCount: 0 };
|
|
118
|
+
}
|
|
93
119
|
return {
|
|
94
120
|
hash: row.projectHash,
|
|
95
121
|
projectId: row.projectId,
|
|
@@ -103,6 +129,10 @@ function rowToProject(row: ProjectRegistryRow): NexusProject {
|
|
|
103
129
|
lastSync: row.lastSync,
|
|
104
130
|
taskCount: row.taskCount,
|
|
105
131
|
labels,
|
|
132
|
+
brainDbPath: row.brainDbPath ?? null,
|
|
133
|
+
tasksDbPath: row.tasksDbPath ?? null,
|
|
134
|
+
lastIndexed: row.lastIndexed ?? null,
|
|
135
|
+
stats,
|
|
106
136
|
};
|
|
107
137
|
}
|
|
108
138
|
|
|
@@ -328,6 +358,9 @@ export async function nexusRegister(
|
|
|
328
358
|
const meta = await readProjectMeta(projectPath);
|
|
329
359
|
const now = new Date().toISOString();
|
|
330
360
|
let projectId = await readProjectId(projectPath);
|
|
361
|
+
const resolvedPath = resolve(projectPath);
|
|
362
|
+
const brainDbPath = join(resolvedPath, '.cleo', 'brain.db');
|
|
363
|
+
const tasksDbPath = join(resolvedPath, '.cleo', 'tasks.db');
|
|
331
364
|
|
|
332
365
|
if (existing) {
|
|
333
366
|
// Merge nexus fields into existing entry
|
|
@@ -339,6 +372,8 @@ export async function nexusRegister(
|
|
|
339
372
|
taskCount: meta.taskCount,
|
|
340
373
|
labelsJson: JSON.stringify(meta.labels),
|
|
341
374
|
lastSeen: now,
|
|
375
|
+
brainDbPath,
|
|
376
|
+
tasksDbPath,
|
|
342
377
|
})
|
|
343
378
|
.where(eq(projectRegistry.projectHash, projectHash));
|
|
344
379
|
} else {
|
|
@@ -361,6 +396,9 @@ export async function nexusRegister(
|
|
|
361
396
|
lastSync: now,
|
|
362
397
|
taskCount: meta.taskCount,
|
|
363
398
|
labelsJson: JSON.stringify(meta.labels),
|
|
399
|
+
brainDbPath,
|
|
400
|
+
tasksDbPath,
|
|
401
|
+
statsJson: '{}',
|
|
364
402
|
});
|
|
365
403
|
}
|
|
366
404
|
|
|
@@ -527,6 +565,70 @@ export async function nexusSyncAll(): Promise<{ synced: number; failed: number }
|
|
|
527
565
|
return { synced, failed };
|
|
528
566
|
}
|
|
529
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Update code intelligence index stats for a registered project.
|
|
570
|
+
*
|
|
571
|
+
* Called after a successful `cleo nexus analyze` run to record the
|
|
572
|
+
* latest node/relation/file counts and the indexed timestamp.
|
|
573
|
+
*
|
|
574
|
+
* @param projectPath - Absolute path to the project root.
|
|
575
|
+
* @param stats - Results from the pipeline run.
|
|
576
|
+
* @task T622
|
|
577
|
+
*/
|
|
578
|
+
export async function nexusUpdateIndexStats(
|
|
579
|
+
projectPath: string,
|
|
580
|
+
stats: NexusProjectStats,
|
|
581
|
+
): Promise<void> {
|
|
582
|
+
if (!projectPath) return;
|
|
583
|
+
|
|
584
|
+
const projectHash = generateProjectHash(projectPath);
|
|
585
|
+
const now = new Date().toISOString();
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
const { getNexusDb } = await import('../store/nexus-sqlite.js');
|
|
589
|
+
const { eq } = await import('drizzle-orm');
|
|
590
|
+
const db = await getNexusDb();
|
|
591
|
+
|
|
592
|
+
const rows = await db
|
|
593
|
+
.select()
|
|
594
|
+
.from(projectRegistry)
|
|
595
|
+
.where(eq(projectRegistry.projectHash, projectHash));
|
|
596
|
+
|
|
597
|
+
if (rows.length === 0) {
|
|
598
|
+
// Not yet registered — auto-register first (best effort)
|
|
599
|
+
try {
|
|
600
|
+
await nexusRegister(projectPath);
|
|
601
|
+
} catch {
|
|
602
|
+
// Already registered or cannot register — ignore
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
await db
|
|
607
|
+
.update(projectRegistry)
|
|
608
|
+
.set({
|
|
609
|
+
lastIndexed: now,
|
|
610
|
+
statsJson: JSON.stringify(stats),
|
|
611
|
+
lastSeen: now,
|
|
612
|
+
})
|
|
613
|
+
.where(eq(projectRegistry.projectHash, projectHash));
|
|
614
|
+
|
|
615
|
+
await writeNexusAudit({
|
|
616
|
+
action: 'update-index-stats',
|
|
617
|
+
projectHash,
|
|
618
|
+
operation: 'update-index-stats',
|
|
619
|
+
success: true,
|
|
620
|
+
details: {
|
|
621
|
+
nodeCount: stats.nodeCount,
|
|
622
|
+
relationCount: stats.relationCount,
|
|
623
|
+
fileCount: stats.fileCount,
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
} catch (err) {
|
|
627
|
+
// Non-fatal — index stats update must never break the analyze pipeline
|
|
628
|
+
getLogger('nexus').warn({ err }, 'nexus: failed to update index stats');
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
530
632
|
/**
|
|
531
633
|
* Update a project's permission level in the registry.
|
|
532
634
|
* Used by permissions.ts to avoid direct JSON file writes.
|
|
@@ -31,11 +31,20 @@ export const projectRegistry = sqliteTable(
|
|
|
31
31
|
lastSync: text('last_sync').notNull().default(sql`(datetime('now'))`),
|
|
32
32
|
taskCount: integer('task_count').notNull().default(0),
|
|
33
33
|
labelsJson: text('labels_json').notNull().default('[]'),
|
|
34
|
+
/** Absolute path to the project's brain.db file. */
|
|
35
|
+
brainDbPath: text('brain_db_path'),
|
|
36
|
+
/** Absolute path to the project's tasks.db file. */
|
|
37
|
+
tasksDbPath: text('tasks_db_path'),
|
|
38
|
+
/** ISO 8601 timestamp of the last successful code intelligence index run. */
|
|
39
|
+
lastIndexed: text('last_indexed'),
|
|
40
|
+
/** JSON object with per-project code intelligence stats (node_count, relation_count, file_count). */
|
|
41
|
+
statsJson: text('stats_json').notNull().default('{}'),
|
|
34
42
|
},
|
|
35
43
|
(table) => [
|
|
36
44
|
index('idx_project_registry_hash').on(table.projectHash),
|
|
37
45
|
index('idx_project_registry_health').on(table.healthStatus),
|
|
38
46
|
index('idx_project_registry_name').on(table.name),
|
|
47
|
+
index('idx_project_registry_last_indexed').on(table.lastIndexed),
|
|
39
48
|
],
|
|
40
49
|
);
|
|
41
50
|
|