@adhdev/daemon-core 0.8.25 → 0.8.28

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.
Files changed (47) hide show
  1. package/dist/agent-stream/provider-adapter.d.ts +1 -0
  2. package/dist/boot/daemon-lifecycle.d.ts +2 -0
  3. package/dist/cli-adapters/pty-transport.d.ts +3 -0
  4. package/dist/commands/handler.d.ts +1 -0
  5. package/dist/commands/router.d.ts +24 -0
  6. package/dist/commands/stream-commands.d.ts +1 -1
  7. package/dist/detection/cli-detector.d.ts +6 -2
  8. package/dist/detection/ide-detector.d.ts +2 -1
  9. package/dist/index.js +824 -369
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +822 -367
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/providers/acp-provider-instance.d.ts +1 -1
  14. package/dist/providers/cli-provider-instance.d.ts +1 -0
  15. package/dist/providers/extension-provider-instance.d.ts +7 -0
  16. package/dist/providers/provider-loader.d.ts +26 -0
  17. package/dist/shared-types.d.ts +2 -0
  18. package/dist/status/normalize.js +14 -2
  19. package/dist/status/normalize.js.map +1 -1
  20. package/dist/status/normalize.mjs +14 -2
  21. package/dist/status/normalize.mjs.map +1 -1
  22. package/dist/status/snapshot.d.ts +8 -2
  23. package/node_modules/@adhdev/session-host-core/dist/index.d.mts +72 -1
  24. package/node_modules/@adhdev/session-host-core/dist/index.d.ts +72 -1
  25. package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
  26. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  27. package/package.json +1 -1
  28. package/src/agent-stream/provider-adapter.ts +45 -3
  29. package/src/boot/daemon-lifecycle.ts +31 -1
  30. package/src/cli-adapters/provider-cli-adapter.ts +14 -3
  31. package/src/cli-adapters/pty-transport.ts +3 -0
  32. package/src/cli-adapters/session-host-transport.ts +8 -0
  33. package/src/commands/chat-commands.ts +38 -9
  34. package/src/commands/cli-manager.ts +2 -2
  35. package/src/commands/handler.ts +26 -3
  36. package/src/commands/router.ts +144 -1
  37. package/src/commands/stream-commands.ts +6 -3
  38. package/src/detection/cli-detector.ts +72 -29
  39. package/src/detection/ide-detector.ts +24 -8
  40. package/src/launch.ts +1 -1
  41. package/src/providers/acp-provider-instance.ts +19 -10
  42. package/src/providers/cli-provider-instance.ts +29 -1
  43. package/src/providers/extension-provider-instance.ts +24 -1
  44. package/src/providers/provider-loader.ts +144 -11
  45. package/src/shared-types.ts +2 -0
  46. package/src/status/normalize.ts +19 -2
  47. package/src/status/snapshot.ts +25 -14
@@ -25,6 +25,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
25
25
  private currentStatus: string = 'idle';
26
26
  private agentStreams: any[] = [];
27
27
  private messages: any[] = [];
28
+ private prevMessageHashes = new Map<string, number>();
28
29
  private activeModal: any = null;
29
30
  private currentModel: string = '';
30
31
  private currentMode: string = '';
@@ -104,7 +105,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
104
105
  if (event === 'stream_update') {
105
106
  // Reflect data collected from agent-stream-manager
106
107
  if (data?.streams) this.agentStreams = data.streams;
107
- if (data?.messages) this.messages = data.messages;
108
+ if (data?.messages) this.messages = this.assignReceivedAt(data.messages);
108
109
  if (data?.activeModal !== undefined) this.activeModal = data.activeModal;
109
110
  if (data?.model) this.currentModel = data.model;
110
111
  if (data?.mode) this.currentMode = data.mode;
@@ -132,6 +133,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
132
133
  dispose(): void {
133
134
  this.agentStreams = [];
134
135
  this.messages = [];
136
+ this.prevMessageHashes.clear();
135
137
  this.monitor.reset();
136
138
  this.appliedEffectKeys.clear();
137
139
  this.runtimeMessages = [];
@@ -315,6 +317,26 @@ export class ExtensionProviderInstance implements ProviderInstance {
315
317
  );
316
318
  }
317
319
 
320
+ /**
321
+ * Assign stable receivedAt to extension messages.
322
+ * Same pattern as IdeProviderInstance.readChat() prevByHash —
323
+ * preserves first-seen timestamp across polling cycles.
324
+ */
325
+ private assignReceivedAt(messages: any[]): any[] {
326
+ const now = Date.now();
327
+ const nextHashes = new Map<string, number>();
328
+
329
+ for (const msg of messages) {
330
+ const hash = `${msg.role}:${(msg.content || '').slice(0, 100)}`;
331
+ const prevTime = this.prevMessageHashes.get(hash);
332
+ msg.receivedAt = prevTime || now;
333
+ nextHashes.set(hash, msg.receivedAt);
334
+ }
335
+
336
+ this.prevMessageHashes = nextHashes;
337
+ return messages;
338
+ }
339
+
318
340
  private mergeConversationMessages(messages: any[]): ChatMessage[] {
319
341
  if (this.runtimeMessages.length === 0) return messages;
320
342
  return [...messages, ...this.runtimeMessages.map((entry) => entry.message)]
@@ -382,6 +404,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
382
404
  }
383
405
  this.agentStreams = [];
384
406
  this.messages = [];
407
+ this.prevMessageHashes.clear();
385
408
  this.activeModal = null;
386
409
  this.currentModel = '';
387
410
  this.currentMode = '';
@@ -24,12 +24,19 @@ import type {
24
24
  ProviderModule,
25
25
  ProviderCategory,
26
26
  ProviderScripts,
27
+ ProviderSettingDef,
27
28
  ProviderSettingSchema,
28
29
  ResolvedProvider,
29
30
  } from './contracts.js';
30
31
 
32
+ interface ProviderAvailabilityState {
33
+ installed: boolean;
34
+ detectedPath: string | null;
35
+ }
36
+
31
37
  export class ProviderLoader {
32
38
  private providers = new Map<string, ProviderModule>();
39
+ private providerAvailability = new Map<string, ProviderAvailabilityState>();
33
40
  private userDir: string;
34
41
  private upstreamDir: string;
35
42
  private disableUpstream: boolean;
@@ -152,6 +159,7 @@ export class ProviderLoader {
152
159
  */
153
160
  loadAll(): void {
154
161
  this.providers.clear();
162
+ this.providerAvailability.clear();
155
163
 
156
164
  // 1. Load upstream (GitHub auto-download — primary source)
157
165
  let upstreamCount = 0;
@@ -236,11 +244,12 @@ export class ProviderLoader {
236
244
  const versionCommand = typeof verCmdConfig === 'object' && verCmdConfig !== null
237
245
  ? verCmdConfig[process.platform]
238
246
  : verCmdConfig;
247
+ const command = this.getSpawnCommand(p.type, p.spawn.command);
239
248
  result.push({
240
249
  id: p.type,
241
250
  displayName: p.displayName || p.name,
242
251
  icon: p.icon || '🔧',
243
- command: p.spawn.command,
252
+ command,
244
253
  category: p.category,
245
254
  ...(typeof versionCommand === 'string' && versionCommand.trim()
246
255
  ? { versionCommand: versionCommand.trim() }
@@ -386,6 +395,80 @@ export class ProviderLoader {
386
395
  .map(p => p.type);
387
396
  }
388
397
 
398
+ getSpawnCommand(type: string, fallback?: string): string {
399
+ const override = this.getOptionalStringSetting(type, 'executablePath');
400
+ if (override) return override;
401
+ return fallback || this.providers.get(type)?.spawn?.command || type;
402
+ }
403
+
404
+ getIdeCliCommand(type: string, fallback?: string | null): string | null {
405
+ const override = this.getOptionalStringSetting(type, 'cliPathOverride');
406
+ if (override) return override;
407
+ return fallback || this.providers.get(type)?.cli || null;
408
+ }
409
+
410
+ getIdePathCandidates(type: string, fallback?: string[]): string[] {
411
+ const override = this.getOptionalStringSetting(type, 'appPathOverride');
412
+ if (override) return [override];
413
+ if (fallback && fallback.length > 0) return fallback;
414
+ const osPaths = this.providers.get(type)?.paths?.[process.platform];
415
+ return Array.isArray(osPaths) ? [...osPaths] : [];
416
+ }
417
+
418
+ setProviderAvailability(type: string, state: { installed: boolean; detectedPath?: string | null }): void {
419
+ this.providerAvailability.set(type, {
420
+ installed: !!state.installed,
421
+ detectedPath: state.detectedPath ?? null,
422
+ });
423
+ }
424
+
425
+ setCliDetectionResults(results: Array<{ id: string; installed: boolean; path?: string }>, replace: boolean = true): void {
426
+ if (replace) {
427
+ for (const provider of this.providers.values()) {
428
+ if (provider.category === 'cli' || provider.category === 'acp') {
429
+ this.providerAvailability.set(provider.type, { installed: false, detectedPath: null });
430
+ }
431
+ }
432
+ }
433
+ for (const result of results) {
434
+ this.setProviderAvailability(result.id, {
435
+ installed: !!result.installed,
436
+ detectedPath: result.path || null,
437
+ });
438
+ }
439
+ }
440
+
441
+ setIdeDetectionResults(results: Array<{ id: string; installed: boolean; path?: string | null; cliCommand?: string | null }>, replace: boolean = true): void {
442
+ if (replace) {
443
+ for (const provider of this.providers.values()) {
444
+ if (provider.category === 'ide') {
445
+ this.providerAvailability.set(provider.type, { installed: false, detectedPath: null });
446
+ }
447
+ }
448
+ }
449
+ for (const result of results) {
450
+ this.setProviderAvailability(result.id, {
451
+ installed: !!result.installed,
452
+ detectedPath: result.cliCommand || result.path || null,
453
+ });
454
+ }
455
+ }
456
+
457
+ getAvailableProviderInfos(): Array<ProviderModule & { installed?: boolean; detectedPath?: string | null }> {
458
+ return this.getAll().map((provider) => {
459
+ const availability = this.providerAvailability.get(provider.type);
460
+ return {
461
+ ...provider,
462
+ ...(availability
463
+ ? {
464
+ installed: availability.installed,
465
+ detectedPath: availability.detectedPath,
466
+ }
467
+ : {}),
468
+ };
469
+ });
470
+ }
471
+
389
472
  /**
390
473
  * Register IDE providers to core/detector registry
391
474
  * → Enables detectIDEs() to detect provider.js-based IDEs
@@ -888,9 +971,8 @@ export class ProviderLoader {
888
971
  * Get public settings schema for a provider (for dashboard UI rendering)
889
972
  */
890
973
  getPublicSettings(type: string): ProviderSettingSchema[] {
891
- const provider = this.providers.get(type);
892
- if (!provider?.settings) return [];
893
- return Object.entries(provider.settings)
974
+ const settings = this.getSettingsSchema(type);
975
+ return Object.entries(settings)
894
976
  .filter(([, def]) => (def as any).public === true)
895
977
  .map(([key, def]) => ({ key, ...(def as any) }));
896
978
  }
@@ -911,8 +993,7 @@ export class ProviderLoader {
911
993
  * Resolved setting value for a provider (default + user override)
912
994
  */
913
995
  getSettingValue(type: string, key: string): any {
914
- const provider = this.providers.get(type);
915
- const schemaDef = provider?.settings?.[key];
996
+ const schemaDef = this.getSettingsSchema(type)[key];
916
997
  const defaultVal = schemaDef ? (schemaDef as any).default : undefined;
917
998
 
918
999
  // Load user-saved value
@@ -930,10 +1011,9 @@ export class ProviderLoader {
930
1011
  * All resolved settings for a provider (default + user override)
931
1012
  */
932
1013
  getSettings(type: string): Record<string, any> {
933
- const provider = this.providers.get(type);
934
- if (!provider?.settings) return {};
1014
+ const settings = this.getSettingsSchema(type);
935
1015
  const result: Record<string, any> = {};
936
- for (const [key, def] of Object.entries(provider.settings)) {
1016
+ for (const [key] of Object.entries(settings)) {
937
1017
  result[key] = this.getSettingValue(type, key);
938
1018
  }
939
1019
  return result;
@@ -943,8 +1023,7 @@ export class ProviderLoader {
943
1023
  * Save provider setting value (writes to config.json)
944
1024
  */
945
1025
  setSetting(type: string, key: string, value: any): boolean {
946
- const provider = this.providers.get(type);
947
- const schemaDef = provider?.settings?.[key] as any;
1026
+ const schemaDef = this.getSettingsSchema(type)[key] as any;
948
1027
  if (!schemaDef) return false;
949
1028
 
950
1029
  // Non-public settings cannot be modified externally
@@ -952,6 +1031,7 @@ export class ProviderLoader {
952
1031
 
953
1032
  // Type validation
954
1033
  if (schemaDef.type === 'boolean' && typeof value !== 'boolean') return false;
1034
+ if (schemaDef.type === 'string' && typeof value !== 'string') return false;
955
1035
  if (schemaDef.type === 'number') {
956
1036
  if (typeof value !== 'number') return false;
957
1037
  if (schemaDef.min !== undefined && value < schemaDef.min) return false;
@@ -974,6 +1054,59 @@ export class ProviderLoader {
974
1054
  }
975
1055
  }
976
1056
 
1057
+ private getOptionalStringSetting(type: string, key: string): string | null {
1058
+ const value = this.getSettingValue(type, key);
1059
+ if (typeof value !== 'string') return null;
1060
+ const trimmed = value.trim();
1061
+ return trimmed ? trimmed : null;
1062
+ }
1063
+
1064
+ private getSettingsSchema(type: string): Record<string, ProviderSettingDef> {
1065
+ const provider = this.providers.get(type);
1066
+ if (!provider) return {};
1067
+ return {
1068
+ ...this.getSyntheticSettings(type, provider),
1069
+ ...(provider.settings || {}),
1070
+ };
1071
+ }
1072
+
1073
+ private getSyntheticSettings(type: string, provider: ProviderModule): Record<string, ProviderSettingDef> {
1074
+ const result: Record<string, ProviderSettingDef> = {};
1075
+
1076
+ if ((provider.category === 'cli' || provider.category === 'acp') && provider.spawn?.command && !provider.settings?.executablePath) {
1077
+ result.executablePath = {
1078
+ type: 'string',
1079
+ default: '',
1080
+ public: true,
1081
+ label: 'Executable path',
1082
+ description: 'Optional absolute path for this provider binary. Leave blank to use the default PATH lookup.',
1083
+ };
1084
+ }
1085
+
1086
+ if (provider.category === 'ide') {
1087
+ if (provider.cli && !provider.settings?.cliPathOverride) {
1088
+ result.cliPathOverride = {
1089
+ type: 'string',
1090
+ default: '',
1091
+ public: true,
1092
+ label: 'CLI path override',
1093
+ description: 'Optional absolute path for the IDE CLI launcher. Leave blank to use the detected default.',
1094
+ };
1095
+ }
1096
+ if (provider.paths && !provider.settings?.appPathOverride) {
1097
+ result.appPathOverride = {
1098
+ type: 'string',
1099
+ default: '',
1100
+ public: true,
1101
+ label: 'App path override',
1102
+ description: 'Optional absolute path for the IDE app bundle or executable. Leave blank to use the default install locations.',
1103
+ };
1104
+ }
1105
+ }
1106
+
1107
+ return result;
1108
+ }
1109
+
977
1110
  // ─── Private ───────────────────────────────────
978
1111
 
979
1112
  /**
@@ -136,6 +136,8 @@ export interface AvailableProviderInfo {
136
136
  category: 'ide' | 'extension' | 'cli' | 'acp';
137
137
  displayName: string;
138
138
  icon: string;
139
+ installed?: boolean;
140
+ detectedPath?: string | null;
139
141
  }
140
142
 
141
143
  /** ACP config option (model/mode/thought_level selection) */
@@ -64,6 +64,23 @@ function trimMessageForStatus(message: unknown, stringLimit: number): unknown {
64
64
  return trimStructuredStrings(message, stringLimit);
65
65
  }
66
66
 
67
+ /**
68
+ * Collapse timestamp / createdAt into receivedAt so downstream consumers
69
+ * only ever need to read a single canonical time field.
70
+ */
71
+ function normalizeMessageTime(message: unknown): unknown {
72
+ if (!message || typeof message !== 'object') return message;
73
+ const msg = message as Record<string, unknown>;
74
+ if (msg.receivedAt == null) {
75
+ const fallback = msg.timestamp ?? msg.createdAt;
76
+ if (fallback != null) {
77
+ const ts = typeof fallback === 'string' ? Date.parse(fallback as string) : Number(fallback);
78
+ if (Number.isFinite(ts) && ts > 0) msg.receivedAt = ts;
79
+ }
80
+ }
81
+ return msg;
82
+ }
83
+
67
84
  function trimMessagesForStatus(messages: unknown[] | null | undefined): unknown[] {
68
85
  if (!Array.isArray(messages) || messages.length === 0) return [];
69
86
 
@@ -72,11 +89,11 @@ function trimMessagesForStatus(messages: unknown[] | null | undefined): unknown[
72
89
  let totalBytes = 0;
73
90
 
74
91
  for (let i = recent.length - 1; i >= 0; i -= 1) {
75
- let normalized = trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_STRING_LIMIT);
92
+ let normalized = normalizeMessageTime(trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_STRING_LIMIT));
76
93
  let size = estimateBytes(normalized);
77
94
 
78
95
  if (size > STATUS_ACTIVE_CHAT_TOTAL_BYTES_LIMIT) {
79
- normalized = trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_FALLBACK_STRING_LIMIT);
96
+ normalized = normalizeMessageTime(trimMessageForStatus(recent[i], STATUS_ACTIVE_CHAT_FALLBACK_STRING_LIMIT));
80
97
  size = estimateBytes(normalized);
81
98
  }
82
99
 
@@ -35,6 +35,14 @@ export interface StatusSnapshotOptions {
35
35
  displayName?: string;
36
36
  category: 'ide' | 'extension' | 'cli' | 'acp';
37
37
  }>;
38
+ getAvailableProviderInfos?: () => Array<{
39
+ type: string;
40
+ icon?: string;
41
+ displayName?: string;
42
+ category: 'ide' | 'extension' | 'cli' | 'acp';
43
+ installed?: boolean;
44
+ detectedPath?: string | null;
45
+ }>;
38
46
  };
39
47
  detectedIdes: Array<{
40
48
  id: string;
@@ -75,12 +83,22 @@ function buildDetectedIdeInfos(
75
83
  function buildAvailableProviders(
76
84
  providerLoader: StatusSnapshotOptions['providerLoader'],
77
85
  ): AvailableProviderInfo[] {
78
- return providerLoader.getAll().map((provider) => ({
86
+ const providers: Array<{
87
+ type: string;
88
+ icon?: string;
89
+ displayName?: string;
90
+ category: 'ide' | 'extension' | 'cli' | 'acp';
91
+ installed?: boolean;
92
+ detectedPath?: string | null;
93
+ }> = providerLoader.getAvailableProviderInfos?.() || providerLoader.getAll();
94
+ return providers.map((provider) => ({
79
95
  type: provider.type,
80
96
  name: provider.displayName || provider.type,
81
97
  displayName: provider.displayName || provider.type,
82
98
  icon: provider.icon || '💻',
83
99
  category: provider.category,
100
+ ...(provider.installed !== undefined ? { installed: provider.installed } : {}),
101
+ ...(provider.detectedPath !== undefined ? { detectedPath: provider.detectedPath } : {}),
84
102
  }));
85
103
  }
86
104
 
@@ -95,17 +113,12 @@ function parseMessageTime(value: unknown): number {
95
113
 
96
114
  function getSessionMessageUpdatedAt(session: {
97
115
  activeChat?: {
98
- messages?: Array<{ timestamp?: number | string; receivedAt?: number | string; createdAt?: number | string }> | null
116
+ messages?: Array<{ receivedAt?: number | string }> | null
99
117
  } | null
100
118
  }) {
101
119
  const lastMessage = session.activeChat?.messages?.at?.(-1);
102
120
  if (!lastMessage) return 0;
103
- return (
104
- parseMessageTime(lastMessage.timestamp)
105
- || parseMessageTime(lastMessage.receivedAt)
106
- || parseMessageTime(lastMessage.createdAt)
107
- || 0
108
- );
121
+ return parseMessageTime(lastMessage.receivedAt) || 0;
109
122
  }
110
123
 
111
124
  export function getSessionCompletionMarker(session: {
@@ -114,9 +127,7 @@ export function getSessionCompletionMarker(session: {
114
127
  role?: string;
115
128
  id?: string;
116
129
  index?: number;
117
- timestamp?: number | string;
118
130
  receivedAt?: number | string;
119
- createdAt?: number | string;
120
131
  _turnKey?: string;
121
132
  }> | null
122
133
  } | null
@@ -124,17 +135,17 @@ export function getSessionCompletionMarker(session: {
124
135
  const lastMessage = session.activeChat?.messages?.at?.(-1) as any;
125
136
  if (!lastMessage) return '';
126
137
  const role = typeof lastMessage.role === 'string' ? lastMessage.role : '';
127
- if (role === 'user' || role === 'human') return '';
138
+ if (role === 'user' || role === 'human' || role === 'system') return '';
128
139
  if (typeof lastMessage._turnKey === 'string' && lastMessage._turnKey) return `turn:${lastMessage._turnKey}`;
129
140
  if (typeof lastMessage.id === 'string' && lastMessage.id) return `id:${lastMessage.id}`;
130
141
  if (typeof lastMessage.index === 'number' && Number.isFinite(lastMessage.index)) return `idx:${lastMessage.index}`;
131
- const timestamp = parseMessageTime(lastMessage.timestamp) || parseMessageTime(lastMessage.receivedAt) || parseMessageTime(lastMessage.createdAt);
142
+ const timestamp = parseMessageTime(lastMessage.receivedAt);
132
143
  return timestamp > 0 ? `ts:${timestamp}` : '';
133
144
  }
134
145
 
135
146
  function getSessionLastUsedAt(session: {
136
147
  activeChat?: {
137
- messages?: Array<{ timestamp?: number | string; receivedAt?: number | string; createdAt?: number | string }> | null
148
+ messages?: Array<{ receivedAt?: number | string }> | null
138
149
  } | null
139
150
  lastUpdated?: number
140
151
  }) {
@@ -171,7 +182,7 @@ function getUnreadState(
171
182
  }
172
183
  const unread = completionMarker
173
184
  ? completionMarker !== seenCompletionMarker
174
- : hasContentChange && lastUsedAt > lastSeenAt && lastRole !== 'user' && lastRole !== 'human';
185
+ : hasContentChange && lastUsedAt > lastSeenAt && lastRole !== 'user' && lastRole !== 'human' && lastRole !== 'system';
175
186
  return { unread, inboxBucket: unread ? 'task_complete' : 'idle' };
176
187
  }
177
188