@adhdev/daemon-core 0.8.98 → 0.8.100

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.
@@ -1295,9 +1295,17 @@ function extractCanonicalHermesMessageTimestamp(message: Record<string, unknown>
1295
1295
  return fallbackTs;
1296
1296
  }
1297
1297
 
1298
- function readExistingHermesSessionStartRecord(historySessionId: string): HistoryMessage | null {
1298
+ function extractTimestampValue(value: unknown): number {
1299
+ const numericTimestamp = Number(value || 0);
1300
+ if (Number.isFinite(numericTimestamp) && numericTimestamp > 0) return numericTimestamp;
1301
+ const stringTimestamp = typeof value === 'string' ? Date.parse(value) : NaN;
1302
+ if (Number.isFinite(stringTimestamp) && stringTimestamp > 0) return stringTimestamp;
1303
+ return 0;
1304
+ }
1305
+
1306
+ function readExistingSessionStartRecord(agentType: string, historySessionId: string): HistoryMessage | null {
1299
1307
  try {
1300
- const dir = path.join(HISTORY_DIR, 'hermes-cli');
1308
+ const dir = path.join(HISTORY_DIR, agentType);
1301
1309
  if (!fs.existsSync(dir)) return null;
1302
1310
  const files = listHistoryFiles(dir, historySessionId).sort();
1303
1311
  for (const file of files) {
@@ -1320,6 +1328,28 @@ function readExistingHermesSessionStartRecord(historySessionId: string): History
1320
1328
  }
1321
1329
  }
1322
1330
 
1331
+ function rewriteCanonicalSavedHistory(agentType: string, historySessionId: string, records: HistoryMessage[]): boolean {
1332
+ if (records.length === 0) return false;
1333
+ try {
1334
+ const dir = path.join(HISTORY_DIR, agentType);
1335
+ fs.mkdirSync(dir, { recursive: true });
1336
+ const prefix = `${historySessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}_`;
1337
+ for (const file of fs.readdirSync(dir)) {
1338
+ if (file.startsWith(prefix) && file.endsWith('.jsonl')) {
1339
+ fs.unlinkSync(path.join(dir, file));
1340
+ }
1341
+ }
1342
+ const targetDate = new Date(records[records.length - 1].receivedAt || Date.now()).toISOString().slice(0, 10);
1343
+ const filePath = path.join(dir, `${prefix}${targetDate}.jsonl`);
1344
+ fs.writeFileSync(filePath, `${records.map((record) => JSON.stringify(record)).join('\n')}\n`, 'utf-8');
1345
+ invalidatePersistedSavedHistoryIndex(agentType, dir);
1346
+ savedHistorySessionCache.delete(agentType.replace(/[^a-zA-Z0-9_-]/g, '_'));
1347
+ return true;
1348
+ } catch {
1349
+ return false;
1350
+ }
1351
+ }
1352
+
1323
1353
  export function rebuildHermesSavedHistoryFromCanonicalSession(historySessionId: string): boolean {
1324
1354
  const normalizedSessionId = normalizeSavedHistorySessionId('hermes-cli', historySessionId);
1325
1355
  if (!normalizedSessionId) return false;
@@ -1335,7 +1365,7 @@ export function rebuildHermesSavedHistoryFromCanonicalSession(historySessionId:
1335
1365
  const canonicalMessages = Array.isArray(raw.messages) ? raw.messages : [];
1336
1366
  const dir = path.join(HISTORY_DIR, 'hermes-cli');
1337
1367
  fs.mkdirSync(dir, { recursive: true });
1338
- const existingSessionStart = readExistingHermesSessionStartRecord(normalizedSessionId);
1368
+ const existingSessionStart = readExistingSessionStartRecord('hermes-cli', normalizedSessionId);
1339
1369
  const records: HistoryMessage[] = [];
1340
1370
  if (existingSessionStart) {
1341
1371
  records.push({
@@ -1392,21 +1422,184 @@ export function rebuildHermesSavedHistoryFromCanonicalSession(historySessionId:
1392
1422
  }
1393
1423
  }
1394
1424
 
1395
- if (records.length === 0) return false;
1425
+ return rewriteCanonicalSavedHistory('hermes-cli', normalizedSessionId, records);
1426
+ } catch {
1427
+ return false;
1428
+ }
1429
+ }
1396
1430
 
1397
- const prefix = `${normalizedSessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}_`;
1398
- for (const file of fs.readdirSync(dir)) {
1399
- if (file.startsWith(prefix) && file.endsWith('.jsonl')) {
1400
- fs.unlinkSync(path.join(dir, file));
1431
+ function resolveClaudeProjectTranscriptPath(historySessionId: string, workspace?: string): string | null {
1432
+ const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
1433
+ if (!fs.existsSync(claudeProjectsDir)) return null;
1434
+ const normalizedWorkspace = typeof workspace === 'string' ? workspace.trim() : '';
1435
+ if (normalizedWorkspace) {
1436
+ const directPath = path.join(claudeProjectsDir, normalizedWorkspace.replace(/[\\/]/g, '-'), `${historySessionId}.jsonl`);
1437
+ if (fs.existsSync(directPath)) return directPath;
1438
+ }
1439
+ const stack = [claudeProjectsDir];
1440
+ while (stack.length > 0) {
1441
+ const current = stack.pop();
1442
+ if (!current) continue;
1443
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
1444
+ const entryPath = path.join(current, entry.name);
1445
+ if (entry.isDirectory()) {
1446
+ stack.push(entryPath);
1447
+ continue;
1448
+ }
1449
+ if (entry.isFile() && entry.name === `${historySessionId}.jsonl`) {
1450
+ return entryPath;
1401
1451
  }
1402
1452
  }
1453
+ }
1454
+ return null;
1455
+ }
1403
1456
 
1404
- const targetDate = new Date(records[records.length - 1].receivedAt || Date.now()).toISOString().slice(0, 10);
1405
- const filePath = path.join(dir, `${prefix}${targetDate}.jsonl`);
1406
- fs.writeFileSync(filePath, `${records.map((record) => JSON.stringify(record)).join('\n')}\n`, 'utf-8');
1407
- invalidatePersistedSavedHistoryIndex('hermes-cli', dir);
1408
- savedHistorySessionCache.delete('hermes-cli');
1409
- return true;
1457
+ function extractClaudeAssistantContentParts(content: unknown): Array<{ content: string; kind: 'standard' | 'tool'; senderName?: string; role?: 'assistant' }> {
1458
+ if (typeof content === 'string') {
1459
+ const trimmed = content.trim();
1460
+ return trimmed ? [{ content: trimmed, kind: 'standard', role: 'assistant' }] : [];
1461
+ }
1462
+ if (!Array.isArray(content)) return [];
1463
+ const parts: Array<{ content: string; kind: 'standard' | 'tool'; senderName?: string; role?: 'assistant' }> = [];
1464
+ for (const block of content) {
1465
+ if (!block || typeof block !== 'object') continue;
1466
+ const record = block as Record<string, unknown>;
1467
+ const type = String(record.type || '').trim();
1468
+ if (type === 'text') {
1469
+ const text = String(record.text || '').trim();
1470
+ if (text) parts.push({ content: text, kind: 'standard', role: 'assistant' });
1471
+ continue;
1472
+ }
1473
+ if (type === 'tool_use') {
1474
+ const name = String(record.name || '').trim() || 'Tool';
1475
+ const input = record.input && typeof record.input === 'object'
1476
+ ? record.input as Record<string, unknown>
1477
+ : null;
1478
+ const command = input ? String(input.command || '').trim() : '';
1479
+ const summary = command ? `${name}: ${command}` : name;
1480
+ if (summary) parts.push({ content: summary, kind: 'tool', senderName: 'Tool', role: 'assistant' });
1481
+ }
1482
+ }
1483
+ return parts;
1484
+ }
1485
+
1486
+ function extractClaudeUserContentParts(content: unknown): Array<{ role: 'user' | 'assistant'; content: string; kind: 'standard' | 'tool'; senderName?: string }> {
1487
+ if (typeof content === 'string') {
1488
+ const trimmed = content.trim();
1489
+ return trimmed ? [{ role: 'user', content: trimmed, kind: 'standard' }] : [];
1490
+ }
1491
+ if (!Array.isArray(content)) return [];
1492
+ const parts: Array<{ role: 'user' | 'assistant'; content: string; kind: 'standard' | 'tool'; senderName?: string }> = [];
1493
+ for (const block of content) {
1494
+ if (!block || typeof block !== 'object') continue;
1495
+ const record = block as Record<string, unknown>;
1496
+ const type = String(record.type || '').trim();
1497
+ if (type === 'text') {
1498
+ const text = String(record.text || '').trim();
1499
+ if (text) parts.push({ role: 'user', content: text, kind: 'standard' });
1500
+ continue;
1501
+ }
1502
+ if (type === 'tool_result') {
1503
+ const rawContent = record.content;
1504
+ const text = typeof rawContent === 'string'
1505
+ ? rawContent.trim()
1506
+ : Array.isArray(rawContent)
1507
+ ? rawContent
1508
+ .map((entry) => {
1509
+ if (typeof entry === 'string') return entry.trim();
1510
+ if (!entry || typeof entry !== 'object') return '';
1511
+ const nested = entry as Record<string, unknown>;
1512
+ if (typeof nested.text === 'string') return nested.text.trim();
1513
+ if (typeof nested.content === 'string') return nested.content.trim();
1514
+ return '';
1515
+ })
1516
+ .filter(Boolean)
1517
+ .join('\n')
1518
+ : '';
1519
+ if (text) parts.push({ role: 'assistant', content: text, kind: 'tool', senderName: 'Tool' });
1520
+ }
1521
+ }
1522
+ return parts;
1523
+ }
1524
+
1525
+ export function rebuildClaudeSavedHistoryFromNativeProject(historySessionId: string, workspace?: string): boolean {
1526
+ const normalizedSessionId = normalizeSavedHistorySessionId('claude-cli', historySessionId);
1527
+ if (!normalizedSessionId) return false;
1528
+
1529
+ try {
1530
+ const transcriptPath = resolveClaudeProjectTranscriptPath(normalizedSessionId, workspace);
1531
+ if (!transcriptPath) return false;
1532
+ const lines = fs.readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
1533
+ const records: HistoryMessage[] = [];
1534
+ const existingSessionStart = readExistingSessionStartRecord('claude-cli', normalizedSessionId);
1535
+ if (existingSessionStart) {
1536
+ records.push({
1537
+ ...existingSessionStart,
1538
+ historySessionId: normalizedSessionId,
1539
+ });
1540
+ }
1541
+ let fallbackTs = Date.now();
1542
+ for (const line of lines) {
1543
+ let parsed: Record<string, unknown> | null = null;
1544
+ try {
1545
+ parsed = JSON.parse(line) as Record<string, unknown>;
1546
+ } catch {
1547
+ parsed = null;
1548
+ }
1549
+ if (!parsed) continue;
1550
+ const parsedSessionId = String(parsed.sessionId || '').trim();
1551
+ if (parsedSessionId && parsedSessionId !== normalizedSessionId) continue;
1552
+ const receivedAt = extractTimestampValue(parsed.timestamp) || fallbackTs;
1553
+ fallbackTs = receivedAt + 1;
1554
+ const parsedWorkspace = String(parsed.cwd || workspace || '').trim();
1555
+ if (records.length === 0 && parsedWorkspace) {
1556
+ records.push({
1557
+ ts: new Date(receivedAt).toISOString(),
1558
+ receivedAt,
1559
+ role: 'system',
1560
+ kind: 'session_start',
1561
+ content: parsedWorkspace,
1562
+ agent: 'claude-cli',
1563
+ historySessionId: normalizedSessionId,
1564
+ workspace: parsedWorkspace,
1565
+ });
1566
+ }
1567
+ const type = String(parsed.type || '').trim();
1568
+ const message = parsed.message && typeof parsed.message === 'object'
1569
+ ? parsed.message as Record<string, unknown>
1570
+ : null;
1571
+ if (type === 'user' && message) {
1572
+ for (const part of extractClaudeUserContentParts(message.content)) {
1573
+ records.push({
1574
+ ts: new Date(receivedAt).toISOString(),
1575
+ receivedAt,
1576
+ role: part.role,
1577
+ content: part.content,
1578
+ kind: part.kind,
1579
+ senderName: part.senderName,
1580
+ agent: 'claude-cli',
1581
+ historySessionId: normalizedSessionId,
1582
+ });
1583
+ }
1584
+ continue;
1585
+ }
1586
+ if (type === 'assistant' && message) {
1587
+ for (const part of extractClaudeAssistantContentParts(message.content)) {
1588
+ records.push({
1589
+ ts: new Date(receivedAt).toISOString(),
1590
+ receivedAt,
1591
+ role: 'assistant',
1592
+ content: part.content,
1593
+ kind: part.kind,
1594
+ senderName: part.senderName,
1595
+ agent: 'claude-cli',
1596
+ historySessionId: normalizedSessionId,
1597
+ });
1598
+ }
1599
+ }
1600
+ }
1601
+
1602
+ return rewriteCanonicalSavedHistory('claude-cli', normalizedSessionId, records);
1410
1603
  } catch {
1411
1604
  return false;
1412
1605
  }
@@ -17,7 +17,7 @@ import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
17
17
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
18
18
  import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
19
19
  import { StatusMonitor } from './status-monitor.js';
20
- import { ChatHistoryWriter, readChatHistory, rebuildHermesSavedHistoryFromCanonicalSession } from '../config/chat-history.js';
20
+ import { ChatHistoryWriter, readChatHistory, rebuildClaudeSavedHistoryFromNativeProject, rebuildHermesSavedHistoryFromCanonicalSession } from '../config/chat-history.js';
21
21
  import { LOG } from '../logging/logger.js';
22
22
  import type { ChatMessage } from '../types.js';
23
23
  import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
@@ -377,7 +377,7 @@ export class CliProviderInstance implements ProviderInstance {
377
377
  : [];
378
378
  }
379
379
  const mergedMessages = this.mergeConversationMessages(parsedMessages);
380
- const canonicalHermesBackedHistory = this.syncCanonicalHermesSavedHistoryIfNeeded();
380
+ const canonicalBackedHistory = this.syncCanonicalSavedHistoryIfNeeded();
381
381
 
382
382
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
383
383
 
@@ -400,7 +400,7 @@ export class CliProviderInstance implements ProviderInstance {
400
400
  senderName: typeof message.senderName === 'string' ? message.senderName : undefined,
401
401
  receivedAt: typeof message.receivedAt === 'number' ? message.receivedAt : message.timestamp,
402
402
  }));
403
- if (!canonicalHermesBackedHistory && !shouldSkipReplayPersist && normalizedMessagesToSave.length > 0) {
403
+ if (!canonicalBackedHistory && !shouldSkipReplayPersist && normalizedMessagesToSave.length > 0) {
404
404
  const incrementalMessages = buildIncrementalHistoryAppendMessages(this.lastPersistedHistoryMessages, normalizedMessagesToSave);
405
405
  this.historyWriter.appendNewMessages(
406
406
  this.type,
@@ -410,7 +410,7 @@ export class CliProviderInstance implements ProviderInstance {
410
410
  this.providerSessionId,
411
411
  );
412
412
  }
413
- if (!canonicalHermesBackedHistory) {
413
+ if (!canonicalBackedHistory) {
414
414
  this.lastPersistedHistoryMessages = normalizedMessagesToSave;
415
415
  }
416
416
  }
@@ -807,7 +807,14 @@ export class CliProviderInstance implements ProviderInstance {
807
807
  get cliName(): string { return this.provider.name; }
808
808
 
809
809
  private shouldAutoApprove(): boolean {
810
- return this.settings.autoApprove !== false;
810
+ if (typeof this.settings.autoApprove === 'boolean') {
811
+ return this.settings.autoApprove;
812
+ }
813
+ const providerDefault = this.provider.settings?.autoApprove?.default;
814
+ if (typeof providerDefault === 'boolean') {
815
+ return providerDefault;
816
+ }
817
+ return false;
811
818
  }
812
819
 
813
820
  private recordAutoApproval(modalMessage?: string, buttonLabel?: string, now = Date.now()): void {
@@ -951,33 +958,53 @@ export class CliProviderInstance implements ProviderInstance {
951
958
  LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
952
959
  }
953
960
 
954
- private syncCanonicalHermesSavedHistoryIfNeeded(): boolean {
955
- if (this.type !== 'hermes-cli' || !this.providerSessionId) return false;
956
- try {
957
- const canonicalPath = path.join(os.homedir(), '.hermes', 'sessions', `session_${this.providerSessionId}.json`);
958
- if (!fs.existsSync(canonicalPath)) return false;
959
- const stat = fs.statSync(canonicalPath);
960
- if (stat.mtimeMs <= this.lastCanonicalHermesSyncMtimeMs) return true;
961
- const rebuilt = rebuildHermesSavedHistoryFromCanonicalSession(this.providerSessionId);
962
- if (!rebuilt) return false;
963
- this.lastCanonicalHermesSyncMtimeMs = stat.mtimeMs;
964
- const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId);
965
- this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
966
- role: message.role,
967
- content: message.content,
968
- kind: message.kind,
969
- senderName: message.senderName,
970
- receivedAt: message.receivedAt,
971
- }));
972
- return true;
973
- } catch {
974
- return false;
961
+ private syncCanonicalSavedHistoryIfNeeded(): boolean {
962
+ if (!this.providerSessionId) return false;
963
+ if (this.type === 'hermes-cli') {
964
+ try {
965
+ const canonicalPath = path.join(os.homedir(), '.hermes', 'sessions', `session_${this.providerSessionId}.json`);
966
+ if (!fs.existsSync(canonicalPath)) return false;
967
+ const stat = fs.statSync(canonicalPath);
968
+ if (stat.mtimeMs <= this.lastCanonicalHermesSyncMtimeMs) return true;
969
+ const rebuilt = rebuildHermesSavedHistoryFromCanonicalSession(this.providerSessionId);
970
+ if (!rebuilt) return false;
971
+ this.lastCanonicalHermesSyncMtimeMs = stat.mtimeMs;
972
+ const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId);
973
+ this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
974
+ role: message.role,
975
+ content: message.content,
976
+ kind: message.kind,
977
+ senderName: message.senderName,
978
+ receivedAt: message.receivedAt,
979
+ }));
980
+ return true;
981
+ } catch {
982
+ return false;
983
+ }
984
+ }
985
+ if (this.type === 'claude-cli') {
986
+ try {
987
+ const rebuilt = rebuildClaudeSavedHistoryFromNativeProject(this.providerSessionId, this.workingDir);
988
+ if (!rebuilt) return false;
989
+ const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId);
990
+ this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
991
+ role: message.role,
992
+ content: message.content,
993
+ kind: message.kind,
994
+ senderName: message.senderName,
995
+ receivedAt: message.receivedAt,
996
+ }));
997
+ return true;
998
+ } catch {
999
+ return false;
1000
+ }
975
1001
  }
1002
+ return false;
976
1003
  }
977
1004
 
978
1005
  private restorePersistedHistoryFromCurrentSession(): void {
979
1006
  if (!this.providerSessionId) return;
980
- this.syncCanonicalHermesSavedHistoryIfNeeded();
1007
+ this.syncCanonicalSavedHistoryIfNeeded();
981
1008
  this.historyWriter.compactHistorySession(this.type, this.providerSessionId);
982
1009
  const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId);
983
1010
  this.historyWriter.seedSessionHistory(
@@ -1,4 +1,5 @@
1
1
  const HERMES_SESSION_ID_RE = /^\d{8}_\d{6}_[a-z0-9]+$/i
2
+ const CLAUDE_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
2
3
 
3
4
  export function normalizeProviderSessionId(providerType: string | undefined, providerSessionId: string | null | undefined): string {
4
5
  const normalizedProviderType = typeof providerType === 'string' ? providerType.trim() : ''
@@ -11,6 +12,9 @@ export function normalizeProviderSessionId(providerType: string | undefined, pro
11
12
  if (normalizedProviderType === 'hermes-cli' && !HERMES_SESSION_ID_RE.test(normalizedId)) {
12
13
  return ''
13
14
  }
15
+ if (normalizedProviderType === 'claude-cli' && !CLAUDE_SESSION_ID_RE.test(normalizedId)) {
16
+ return ''
17
+ }
14
18
 
15
19
  return normalizedId
16
20
  }