lumencode 1.1.0 → 1.2.0
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/lib/git.js +271 -2
- package/lib/server.js +36 -1
- package/package.json +1 -1
package/lib/git.js
CHANGED
|
@@ -180,7 +180,7 @@ function resolveToolFromSignals(signals) {
|
|
|
180
180
|
return null;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
function createAIAttribution({ confidence = AI_CONFIDENCE.NONE, signals = [], attributionType = null, detectedTool = null } = {}) {
|
|
183
|
+
function createAIAttribution({ confidence = AI_CONFIDENCE.NONE, signals = [], attributionType = null, detectedTool = null, negativeSignals = [] } = {}) {
|
|
184
184
|
const normalizedSignals = [...new Set(signals)];
|
|
185
185
|
return {
|
|
186
186
|
isAI: isCountedAIConfidence(confidence),
|
|
@@ -191,6 +191,7 @@ function createAIAttribution({ confidence = AI_CONFIDENCE.NONE, signals = [], at
|
|
|
191
191
|
aiEvidence: normalizedSignals,
|
|
192
192
|
attributionType,
|
|
193
193
|
detectedTool,
|
|
194
|
+
negativeSignals,
|
|
194
195
|
};
|
|
195
196
|
}
|
|
196
197
|
|
|
@@ -248,6 +249,203 @@ function loadAttributionOverrides() {
|
|
|
248
249
|
}
|
|
249
250
|
}
|
|
250
251
|
|
|
252
|
+
// ── Style-based heuristic detection (fallback when no explicit signals) ──
|
|
253
|
+
|
|
254
|
+
const IMPERATIVE_VERBS = /^(?:add|fix|update|remove|refactor|implement|create|delete|replace|rename|move|extract|improve|optimize|simplify|restructure|rewrite|migrate|upgrade|downgrade|revert|bump|clean|format|lint|docs?|test|build|ci|chore|perf|style|init|setup|config|configure|enable|disable|support|handle|ensure|validate|verify|check|detect|parse|compute|merge|split|group|sort|filter|map|reduce|export|import|load|save|read|write|reset|toggle|switch|convert|transform|wrap|unwrap|escape|unescape|encode|decode|serialize|deserialize|compress|decompress|encrypt|decrypt|hash|sign|verify|auth|login|logout|register|unregister|subscribe|unsubscribe|connect|disconnect|bind|unbind|attach|detach|mount|unmount|open|close|show|hide|display|render|draw|paint|print|log|trace|debug|info|warn|error|fatal|throw|catch|retry|abort|cancel|timeout|expire|flush|clear|reset|restore|backup|archive|deploy|release|publish|install|uninstall)\b/i;
|
|
255
|
+
|
|
256
|
+
function detectStyleSignals(subject, body) {
|
|
257
|
+
const signals = [];
|
|
258
|
+
let score = 0;
|
|
259
|
+
|
|
260
|
+
// Bullet list: 3+ lines starting with "- " or "* "
|
|
261
|
+
const bulletLines = (body || '').split('\n').filter(l => /^\s*[-*]\s+\S/.test(l));
|
|
262
|
+
if (bulletLines.length >= 3) {
|
|
263
|
+
signals.push('styleBulletList');
|
|
264
|
+
score += 2;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Conventional commit with scope: type(scope): subject
|
|
268
|
+
if (CONVENTIONAL_RE.test(subject)) {
|
|
269
|
+
const m = subject.match(CONVENTIONAL_RE);
|
|
270
|
+
if (m && m[2]) {
|
|
271
|
+
signals.push('styleConventionalScope');
|
|
272
|
+
score += 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Long structured body: >150 chars with 3+ non-empty lines
|
|
277
|
+
const bodyLines = (body || '').split('\n').filter(l => l.trim());
|
|
278
|
+
if ((body || '').length > 150 && bodyLines.length >= 3) {
|
|
279
|
+
signals.push('styleLongStructuredBody');
|
|
280
|
+
score += 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Technical detail: body contains file paths or parenthetical notes
|
|
284
|
+
if (/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+\.[a-z]{1,4}/.test(body) || /\([^)]{10,}\)/.test(body)) {
|
|
285
|
+
signals.push('styleTechnicalDetail');
|
|
286
|
+
score += 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Imperative mood: 3+ body lines start with imperative verbs (strip bullet prefix first)
|
|
290
|
+
const imperativeCount = bodyLines.filter(l => IMPERATIVE_VERBS.test(l.trim().replace(/^[-*]\s+/, ''))).length;
|
|
291
|
+
if (imperativeCount >= 3) {
|
|
292
|
+
signals.push('styleImperativeMood');
|
|
293
|
+
score += 1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { signals, score };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ── Negative signals (reduce false positives) ──
|
|
300
|
+
|
|
301
|
+
const WIP_RE = /\b(?:wip|draft|todo|temp|tmp|hack|quick)\b/i;
|
|
302
|
+
const MERGE_RE = /^Merge\s+(?:branch|pull|remote|tag)/i;
|
|
303
|
+
|
|
304
|
+
export function detectNegativeSignals(subject, body, linesAdded, linesDeleted, fileCount) {
|
|
305
|
+
const signals = [];
|
|
306
|
+
const trimmedBody = (body || '').trim();
|
|
307
|
+
const trimmedSubject = (subject || '').trim();
|
|
308
|
+
const totalLines = (linesAdded || 0) + (linesDeleted || 0);
|
|
309
|
+
const files = fileCount || 0;
|
|
310
|
+
|
|
311
|
+
// Informal: very short subject AND body, no conventional commit prefix
|
|
312
|
+
const isConventional = CONVENTIONAL_RE.test(trimmedSubject);
|
|
313
|
+
if (trimmedSubject.length <= 10 && trimmedBody.length <= 10 && !/\n/.test(trimmedBody) && !isConventional) {
|
|
314
|
+
signals.push('humanInformal');
|
|
315
|
+
}
|
|
316
|
+
if (MERGE_RE.test(trimmedSubject)) {
|
|
317
|
+
signals.push('humanMergeCommit');
|
|
318
|
+
}
|
|
319
|
+
if (totalLines <= 2 && files <= 1) {
|
|
320
|
+
signals.push('humanSmallScope');
|
|
321
|
+
}
|
|
322
|
+
if (WIP_RE.test(trimmedSubject)) {
|
|
323
|
+
signals.push('humanWIP');
|
|
324
|
+
}
|
|
325
|
+
return signals;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Developer behavioral baseline ──
|
|
329
|
+
|
|
330
|
+
function computeAuthorBaseline(commitList) {
|
|
331
|
+
const authorStats = new Map();
|
|
332
|
+
for (const c of commitList || []) {
|
|
333
|
+
const email = c.author || 'unknown';
|
|
334
|
+
if (!authorStats.has(email)) {
|
|
335
|
+
authorStats.set(email, {
|
|
336
|
+
count: 0, totalSubjectLen: 0, totalBodyLen: 0,
|
|
337
|
+
bulletCount: 0, convCount: 0, totalFiles: 0, totalLines: 0,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
const s = authorStats.get(email);
|
|
341
|
+
s.count++;
|
|
342
|
+
s.totalSubjectLen += (c.subject || '').length;
|
|
343
|
+
s.totalBodyLen += (c.body || '').length;
|
|
344
|
+
s.totalFiles += (c.files || []).length;
|
|
345
|
+
s.totalLines += (c.linesAdded || 0) + (c.linesDeleted || 0);
|
|
346
|
+
if (/^\s*[-*]\s+\S/m.test(c.body || '')) s.bulletCount++;
|
|
347
|
+
if (CONVENTIONAL_RE.test(c.subject)) s.convCount++;
|
|
348
|
+
}
|
|
349
|
+
const baselines = new Map();
|
|
350
|
+
for (const [email, s] of authorStats) {
|
|
351
|
+
if (s.count < 3) { baselines.set(email, null); continue; }
|
|
352
|
+
baselines.set(email, {
|
|
353
|
+
avgSubjectLen: s.totalSubjectLen / s.count,
|
|
354
|
+
avgBodyLen: s.totalBodyLen / s.count,
|
|
355
|
+
bulletRatio: s.bulletCount / s.count,
|
|
356
|
+
convRatio: s.convCount / s.count,
|
|
357
|
+
avgFiles: s.totalFiles / s.count,
|
|
358
|
+
avgLines: s.totalLines / s.count,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return baselines;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function computeBaselineDeviation(commit, baseline) {
|
|
365
|
+
if (!baseline) return 0;
|
|
366
|
+
let deviation = 0;
|
|
367
|
+
let factors = 0;
|
|
368
|
+
const body = commit.body || '';
|
|
369
|
+
|
|
370
|
+
if (baseline.avgBodyLen > 0) {
|
|
371
|
+
const ratio = body.length / baseline.avgBodyLen;
|
|
372
|
+
deviation += ratio > 2 ? 0.3 : (ratio > 1.5 ? 0.15 : 0);
|
|
373
|
+
factors++;
|
|
374
|
+
}
|
|
375
|
+
const hasBullets = /^\s*[-*]\s+\S/m.test(body);
|
|
376
|
+
if (baseline.bulletRatio < 0.1 && hasBullets) { deviation += 0.3; factors++; }
|
|
377
|
+
const fileCount = (commit.files || []).length;
|
|
378
|
+
if (baseline.avgFiles > 0) {
|
|
379
|
+
const ratio = fileCount / baseline.avgFiles;
|
|
380
|
+
deviation += ratio > 3 ? 0.2 : (ratio > 2 ? 0.1 : 0);
|
|
381
|
+
factors++;
|
|
382
|
+
}
|
|
383
|
+
const lineCount = (commit.linesAdded || 0) + (commit.linesDeleted || 0);
|
|
384
|
+
if (baseline.avgLines > 0) {
|
|
385
|
+
const ratio = lineCount / baseline.avgLines;
|
|
386
|
+
deviation += ratio > 3 ? 0.2 : (ratio > 2 ? 0.1 : 0);
|
|
387
|
+
factors++;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return factors > 0 ? Math.min(deviation / factors, 1) : 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── Composite continuous scoring ──
|
|
394
|
+
|
|
395
|
+
function computeContinuousScore(commit) {
|
|
396
|
+
let score = 0;
|
|
397
|
+
const signals = new Set(commit.aiSignals || []);
|
|
398
|
+
|
|
399
|
+
// Explicit signatures
|
|
400
|
+
if (signals.has('coAuthor') || signals.has('generatedWith') || signals.has('assistedBy')) score += 0.85;
|
|
401
|
+
if (signals.has('coAuthorCopilot') || signals.has('coAuthorCursor') || signals.has('coAuthorCodex')) score += 0.85;
|
|
402
|
+
if (signals.has('robotEmoji') || signals.has('coAuthorOpencode')) score += 0.85;
|
|
403
|
+
if (signals.has('authorClaude') || signals.has('authorBot')) score += 0.80;
|
|
404
|
+
if (signals.has('generatedWithAider') || signals.has('aiderTag')) score += 0.85;
|
|
405
|
+
if (signals.has('generatedWithCodex') || signals.has('coAuthorCodex')) score += 0.85;
|
|
406
|
+
if (signals.has('coAuthorWindsurf') || signals.has('coAuthorAugment') || signals.has('coAuthorCline')) score += 0.85;
|
|
407
|
+
if (signals.has('aiGenerated') || signals.has('generatedByAI') || signals.has('viaAI') || signals.has('aiTag')) score += 0.70;
|
|
408
|
+
|
|
409
|
+
// Session signals
|
|
410
|
+
if (commit.sessionAttribution === 'strong') score += 0.40;
|
|
411
|
+
else if (commit.sessionAttribution === 'cross-day') score += 0.25;
|
|
412
|
+
else if (commit.sessionAttribution === 'weak') score += 0.15;
|
|
413
|
+
else if (commit.sessionAttribution === 'cross-day-weak') score += 0.10;
|
|
414
|
+
|
|
415
|
+
// File overlap
|
|
416
|
+
const overlap = commit.aiEvidenceDetails?.fileOverlapRatio || 0;
|
|
417
|
+
score += overlap * 0.30;
|
|
418
|
+
|
|
419
|
+
// Style heuristic
|
|
420
|
+
if (signals.has('styleBulletList')) score += 0.15;
|
|
421
|
+
if (signals.has('styleConventionalScope')) score += 0.05;
|
|
422
|
+
if (signals.has('styleImperativeMood')) score += 0.10;
|
|
423
|
+
if (signals.has('styleLongStructuredBody')) score += 0.05;
|
|
424
|
+
|
|
425
|
+
// Baseline deviation
|
|
426
|
+
if (signals.has('baselineDeviationHigh')) score += 0.15;
|
|
427
|
+
else if (signals.has('baselineDeviationMedium')) score += 0.08;
|
|
428
|
+
|
|
429
|
+
// Negative signals
|
|
430
|
+
const negSignals = new Set(commit.negativeSignals || []);
|
|
431
|
+
if (negSignals.has('humanMergeCommit')) score -= 0.50;
|
|
432
|
+
if (negSignals.has('humanInformal')) score -= 0.20;
|
|
433
|
+
if (negSignals.has('humanSmallScope')) score -= 0.15;
|
|
434
|
+
if (negSignals.has('humanWIP')) score -= 0.15;
|
|
435
|
+
|
|
436
|
+
// Baseline match (human pattern)
|
|
437
|
+
if (signals.has('humanBaselineMatch')) score -= 0.10;
|
|
438
|
+
|
|
439
|
+
return Math.max(0, Math.min(1, score));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function scoreToConfidence(score) {
|
|
443
|
+
if (score >= 0.75) return AI_CONFIDENCE.HIGH;
|
|
444
|
+
if (score >= 0.45) return AI_CONFIDENCE.MEDIUM;
|
|
445
|
+
if (score >= 0.20) return AI_CONFIDENCE.LOW;
|
|
446
|
+
return AI_CONFIDENCE.NONE;
|
|
447
|
+
}
|
|
448
|
+
|
|
251
449
|
export function detectAICommit(subject = '', author = '', body = '') {
|
|
252
450
|
const haystack = `${subject}\n${body}`;
|
|
253
451
|
const signals = [];
|
|
@@ -285,6 +483,32 @@ export function detectAICommit(subject = '', author = '', body = '') {
|
|
|
285
483
|
detectedTool,
|
|
286
484
|
});
|
|
287
485
|
}
|
|
486
|
+
|
|
487
|
+
// Negative signals: skip style heuristic for obvious human patterns
|
|
488
|
+
// Only check message-level signals here (line/file counts not available)
|
|
489
|
+
const subjectNeg = (subject || '').trim();
|
|
490
|
+
const bodyNeg = (body || '').trim();
|
|
491
|
+
const negSignals = [];
|
|
492
|
+
const isConvSubject = CONVENTIONAL_RE.test(subjectNeg);
|
|
493
|
+
if (subjectNeg.length <= 10 && bodyNeg.length <= 10 && !/\n/.test(bodyNeg) && !isConvSubject) negSignals.push('humanInformal');
|
|
494
|
+
if (/^Merge\s+(?:branch|pull|remote|tag)/i.test(subjectNeg)) negSignals.push('humanMergeCommit');
|
|
495
|
+
if (/\b(?:wip|draft|todo|temp|tmp|hack|quick)\b/i.test(subjectNeg)) negSignals.push('humanWIP');
|
|
496
|
+
if (negSignals.length > 0) {
|
|
497
|
+
return createAIAttribution({ negativeSignals: negSignals });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Fallback: style-based heuristic detection
|
|
501
|
+
const { signals: styleSignals, score } = detectStyleSignals(subject, body);
|
|
502
|
+
if (styleSignals.length > 0 && score >= 4) {
|
|
503
|
+
const confidence = score >= 6 ? AI_CONFIDENCE.MEDIUM : AI_CONFIDENCE.LOW;
|
|
504
|
+
return createAIAttribution({
|
|
505
|
+
confidence,
|
|
506
|
+
signals: styleSignals,
|
|
507
|
+
attributionType: score >= 6 ? 'style_heuristic_strong' : 'style_heuristic',
|
|
508
|
+
detectedTool: null,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
288
512
|
return createAIAttribution();
|
|
289
513
|
}
|
|
290
514
|
|
|
@@ -442,6 +666,7 @@ export function parseGitLogOutput(output, repo = '') {
|
|
|
442
666
|
current.aiEvidence = ai.aiEvidence;
|
|
443
667
|
current.attributionType = ai.attributionType;
|
|
444
668
|
current.detectedTool = ai.detectedTool || null;
|
|
669
|
+
current.negativeSignals = ai.negativeSignals || [];
|
|
445
670
|
current.sessionId = null; // 由 finalize 阶段填充
|
|
446
671
|
|
|
447
672
|
result.commits++;
|
|
@@ -474,7 +699,12 @@ export function parseGitLogOutput(output, repo = '') {
|
|
|
474
699
|
const header = line.slice(COMMIT_SENTINEL.length);
|
|
475
700
|
const parts = header.split('|');
|
|
476
701
|
const hash = parts[0] || '';
|
|
477
|
-
const
|
|
702
|
+
const dateRaw = (parts[1] || '');
|
|
703
|
+
// Normalize to UTC ISO for consistent comparison with session dates
|
|
704
|
+
const dateMs = Date.parse(dateRaw);
|
|
705
|
+
const date = Number.isFinite(dateMs)
|
|
706
|
+
? new Date(dateMs).toISOString().slice(0, 19) + 'Z'
|
|
707
|
+
: dateRaw.slice(0, 19);
|
|
478
708
|
const author = parts[2] || '';
|
|
479
709
|
const subject = parts.slice(3).join('|');
|
|
480
710
|
current = {
|
|
@@ -1031,6 +1261,9 @@ export function finalizeGitStats(merged, sessions = [], options = {}) {
|
|
|
1031
1261
|
}
|
|
1032
1262
|
}
|
|
1033
1263
|
|
|
1264
|
+
// Step 1.5: compute developer behavioral baselines
|
|
1265
|
+
const authorBaselines = computeAuthorBaseline(merged.commitList);
|
|
1266
|
+
|
|
1034
1267
|
// Step 2: 信心度评估(保持现有逻辑)
|
|
1035
1268
|
for (const c of merged.commitList || []) {
|
|
1036
1269
|
if (!c.sessionId || c.attributionType === 'explicit') continue;
|
|
@@ -1096,6 +1329,30 @@ export function finalizeGitStats(merged, sessions = [], options = {}) {
|
|
|
1096
1329
|
signals.push('strongWithoutFileOverlap');
|
|
1097
1330
|
}
|
|
1098
1331
|
|
|
1332
|
+
// Developer baseline deviation
|
|
1333
|
+
const baseline = authorBaselines.get(c.author);
|
|
1334
|
+
const deviation = computeBaselineDeviation(c, baseline);
|
|
1335
|
+
if (deviation >= 0.4) {
|
|
1336
|
+
signals.push('baselineDeviationHigh');
|
|
1337
|
+
if (confidence === AI_CONFIDENCE.MEDIUM) confidence = pickHigherConfidence(confidence, AI_CONFIDENCE.HIGH);
|
|
1338
|
+
} else if (deviation >= 0.2) {
|
|
1339
|
+
signals.push('baselineDeviationMedium');
|
|
1340
|
+
} else if (deviation <= 0.05 && baseline && c.attributionType !== 'explicit') {
|
|
1341
|
+
signals.push('humanBaselineMatch');
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Negative signals: downgrade confidence
|
|
1345
|
+
const negSignals = detectNegativeSignals(c.subject, c.body, c.linesAdded, c.linesDeleted, (c.files || []).length);
|
|
1346
|
+
if (negSignals.includes('humanMergeCommit')) {
|
|
1347
|
+
confidence = AI_CONFIDENCE.NONE;
|
|
1348
|
+
attributionType = 'human_merge';
|
|
1349
|
+
} else if (negSignals.length > 0) {
|
|
1350
|
+
if (confidence === AI_CONFIDENCE.HIGH) confidence = AI_CONFIDENCE.MEDIUM;
|
|
1351
|
+
else if (confidence === AI_CONFIDENCE.MEDIUM) confidence = AI_CONFIDENCE.LOW;
|
|
1352
|
+
else if (confidence === AI_CONFIDENCE.LOW) confidence = AI_CONFIDENCE.NONE;
|
|
1353
|
+
c.negativeSignals = negSignals;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1099
1356
|
const ai = createAIAttribution({
|
|
1100
1357
|
confidence,
|
|
1101
1358
|
signals,
|
|
@@ -1109,6 +1366,18 @@ export function finalizeGitStats(merged, sessions = [], options = {}) {
|
|
|
1109
1366
|
c.attributionType = ai.attributionType;
|
|
1110
1367
|
}
|
|
1111
1368
|
|
|
1369
|
+
// Step 2.5: composite continuous scoring for all commits
|
|
1370
|
+
for (const c of merged.commitList || []) {
|
|
1371
|
+
c.aiScore = computeContinuousScore(c);
|
|
1372
|
+
const mappedConfidence = scoreToConfidence(c.aiScore);
|
|
1373
|
+
// Only override if no explicit signature and continuous score disagrees
|
|
1374
|
+
if (c.attributionType !== 'explicit') {
|
|
1375
|
+
c.aiConfidence = pickHigherConfidence(c.aiConfidence, mappedConfidence);
|
|
1376
|
+
c.isAI = isCountedAIConfidence(c.aiConfidence);
|
|
1377
|
+
c.aiAssisted = c.aiConfidence !== AI_CONFIDENCE.NONE;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1112
1381
|
const attributionItems = [];
|
|
1113
1382
|
for (const c of merged.commitList || []) {
|
|
1114
1383
|
const commitOverride = mergedOverrides.commits[c.hash] || null;
|
package/lib/server.js
CHANGED
|
@@ -562,7 +562,42 @@ export function startServer(config, effectiveIncludeProjects, buildReportData, c
|
|
|
562
562
|
});
|
|
563
563
|
|
|
564
564
|
server.listen(PORT, '127.0.0.1', () => {
|
|
565
|
-
|
|
565
|
+
const B = '\x1b[1m';
|
|
566
|
+
const R = '\x1b[0m';
|
|
567
|
+
const cyan = '\x1b[96m';
|
|
568
|
+
const green = '\x1b[92m';
|
|
569
|
+
const yellow = '\x1b[93m';
|
|
570
|
+
const blue = '\x1b[94m';
|
|
571
|
+
const dim = '\x1b[2m';
|
|
572
|
+
|
|
573
|
+
const banner = [
|
|
574
|
+
'',
|
|
575
|
+
`${B}${cyan} _ _____ _ ${R}`,
|
|
576
|
+
`${B}${cyan} | | / ____| | | ${R}`,
|
|
577
|
+
`${B}${cyan} | | _ _ _ __ ___ ___ _ __ | | ___ __| | ___ ${R}`,
|
|
578
|
+
`${B}${cyan} | | | | | | '_ \` _ \\ / _ \\ '_ \\| | / _ \\ / _\` |/ _ \\${R}`,
|
|
579
|
+
`${B}${cyan} | |___| |_| | | | | | | __/ | | | |___| (_) | (_| | __/${R}`,
|
|
580
|
+
`${B}${cyan} |______\\__,_|_| |_| |_|\\___|_| |_|\\_____\\___/ \\__,_|\\___|${R}`,
|
|
581
|
+
'',
|
|
582
|
+
].join('\n');
|
|
583
|
+
|
|
584
|
+
process.stdout.write(banner + '\n');
|
|
585
|
+
process.stdout.write(` ${green}${B}v${appVersion}${R} ${yellow}AI Coding Assistant Analytics${R}\n`);
|
|
586
|
+
process.stdout.write('\n');
|
|
587
|
+
|
|
588
|
+
if (config.claudeDir) {
|
|
589
|
+
process.stdout.write(` ${dim}●${R} ${B}Data Dir${R} ${config.claudeDir}\n`);
|
|
590
|
+
}
|
|
591
|
+
if (configPath) {
|
|
592
|
+
process.stdout.write(` ${dim}●${R} ${B}Config${R} ${configPath}\n`);
|
|
593
|
+
}
|
|
594
|
+
const repoCount = config.repos?.length || 0;
|
|
595
|
+
if (repoCount > 0) {
|
|
596
|
+
process.stdout.write(` ${dim}●${R} ${B}Projects${R} ${repoCount} repo(s) detected\n`);
|
|
597
|
+
}
|
|
598
|
+
process.stdout.write('\n');
|
|
599
|
+
process.stdout.write(` ${green}${B}✓${R} Server ready at ${blue}${B}http://localhost:${PORT}${R}\n`);
|
|
600
|
+
process.stdout.write('\n');
|
|
566
601
|
|
|
567
602
|
// Auto-open browser
|
|
568
603
|
const openCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|