@adhdev/daemon-core 0.8.27 → 0.8.29
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/agent-stream/manager.d.ts +1 -1
- package/dist/agent-stream/provider-adapter.d.ts +5 -0
- package/dist/commands/handler.d.ts +1 -0
- package/dist/commands/router.d.ts +5 -0
- package/dist/commands/stream-commands.d.ts +1 -1
- package/dist/config/chat-history.d.ts +12 -0
- package/dist/detection/cli-detector.d.ts +6 -2
- package/dist/detection/ide-detector.d.ts +2 -1
- package/dist/index.js +987 -377
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +985 -375
- package/dist/index.mjs.map +1 -1
- package/dist/providers/acp-provider-instance.d.ts +1 -1
- package/dist/providers/cli-provider-instance.d.ts +1 -0
- package/dist/providers/provider-loader.d.ts +26 -0
- package/dist/shared-types.d.ts +2 -0
- package/dist/status/snapshot.d.ts +8 -0
- package/node_modules/@adhdev/session-host-core/dist/index.d.mts +24 -1
- package/node_modules/@adhdev/session-host-core/dist/index.d.ts +24 -1
- package/node_modules/@adhdev/session-host-core/dist/index.js +6 -1
- package/node_modules/@adhdev/session-host-core/dist/index.js.map +1 -1
- package/node_modules/@adhdev/session-host-core/dist/index.mjs +6 -1
- package/node_modules/@adhdev/session-host-core/dist/index.mjs.map +1 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +1 -1
- package/src/agent-stream/manager.ts +2 -2
- package/src/agent-stream/provider-adapter.ts +111 -4
- package/src/boot/daemon-lifecycle.ts +28 -1
- package/src/cli-adapters/provider-cli-adapter.ts +17 -3
- package/src/commands/chat-commands.ts +89 -10
- package/src/commands/cli-manager.ts +16 -2
- package/src/commands/handler.ts +1 -0
- package/src/commands/router.ts +23 -1
- package/src/commands/stream-commands.ts +6 -3
- package/src/config/chat-history.ts +269 -18
- package/src/detection/cli-detector.ts +72 -29
- package/src/detection/ide-detector.ts +24 -8
- package/src/launch.ts +1 -1
- package/src/providers/acp-provider-instance.ts +19 -10
- package/src/providers/cli-provider-instance.ts +17 -2
- package/src/providers/provider-loader.ts +144 -11
- package/src/shared-types.ts +2 -0
- package/src/status/snapshot.ts +19 -1
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { exec } from 'child_process';
|
|
11
11
|
import * as os from 'os';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { existsSync } from 'fs';
|
|
12
14
|
import type { ProviderLoader } from '../providers/provider-loader.js';
|
|
13
15
|
|
|
14
16
|
export interface CLIInfo {
|
|
@@ -28,6 +30,33 @@ function parseVersion(raw: string): string {
|
|
|
28
30
|
return match ? match[1] : raw.split('\n')[0].slice(0, 100);
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
function shellQuote(value: string): string {
|
|
34
|
+
if (/^[a-zA-Z0-9_./:@%+=,-]+$/.test(value)) return value;
|
|
35
|
+
return `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function expandHome(value: string): string {
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
if (!trimmed.startsWith('~')) return trimmed;
|
|
41
|
+
return path.join(os.homedir(), trimmed.slice(1));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isExplicitCommandPath(command: string): boolean {
|
|
45
|
+
const trimmed = command.trim();
|
|
46
|
+
return path.isAbsolute(trimmed) || trimmed.includes('/') || trimmed.includes('\\') || trimmed.startsWith('~');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveCommandPath(command: string): string | null {
|
|
50
|
+
const trimmed = command.trim();
|
|
51
|
+
if (!trimmed) return null;
|
|
52
|
+
if (isExplicitCommandPath(trimmed)) {
|
|
53
|
+
const expanded = expandHome(trimmed);
|
|
54
|
+
const candidate = path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
|
|
55
|
+
return existsSync(candidate) ? candidate : null;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
/** Run a shell command with timeout, returning stdout or null on failure */
|
|
32
61
|
function execAsync(cmd: string, timeoutMs = 5000): Promise<string | null> {
|
|
33
62
|
return new Promise((resolve) => {
|
|
@@ -47,9 +76,13 @@ function execAsync(cmd: string, timeoutMs = 5000): Promise<string | null> {
|
|
|
47
76
|
* Detect all CLI/ACP agents (parallel)
|
|
48
77
|
* @param providerLoader ProviderLoader instance (dynamic list creation)
|
|
49
78
|
*/
|
|
50
|
-
export async function detectCLIs(
|
|
79
|
+
export async function detectCLIs(
|
|
80
|
+
providerLoader?: ProviderLoader,
|
|
81
|
+
options?: { includeVersion?: boolean },
|
|
82
|
+
): Promise<CLIInfo[]> {
|
|
51
83
|
const platform = os.platform();
|
|
52
84
|
const whichCmd = platform === 'win32' ? 'where' : 'which';
|
|
85
|
+
const includeVersion = options?.includeVersion !== false;
|
|
53
86
|
|
|
54
87
|
// Provider-based dynamic list creation, fallback is empty array
|
|
55
88
|
const cliList = providerLoader
|
|
@@ -60,28 +93,31 @@ export async function detectCLIs(providerLoader?: ProviderLoader): Promise<CLIIn
|
|
|
60
93
|
const results = await Promise.all(
|
|
61
94
|
cliList.map(async (cli): Promise<CLIInfo> => {
|
|
62
95
|
try {
|
|
63
|
-
const
|
|
96
|
+
const explicitPath = resolveCommandPath(cli.command);
|
|
97
|
+
const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(cli.command)}`);
|
|
64
98
|
if (!pathResult) return { ...cli, installed: false };
|
|
65
99
|
|
|
66
|
-
const firstPath = pathResult.split('\n')[0];
|
|
100
|
+
const firstPath = explicitPath || pathResult.split('\n')[0];
|
|
67
101
|
|
|
68
102
|
// Get version (parallel with other checks)
|
|
69
103
|
let version: string | undefined;
|
|
70
|
-
|
|
104
|
+
if (includeVersion) {
|
|
71
105
|
const versionCommands = [
|
|
106
|
+
`"${firstPath}" --version`,
|
|
107
|
+
`"${firstPath}" -V`,
|
|
108
|
+
`"${firstPath}" -v`,
|
|
72
109
|
cli.versionCommand,
|
|
73
|
-
`${cli.command} --version`,
|
|
74
|
-
`${cli.command} -V`,
|
|
75
|
-
`${cli.command} -v`,
|
|
76
110
|
].filter((v): v is string => !!v);
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
111
|
+
try {
|
|
112
|
+
for (const versionCommand of versionCommands) {
|
|
113
|
+
const versionResult = await execAsync(versionCommand, 3000);
|
|
114
|
+
if (versionResult) {
|
|
115
|
+
version = parseVersion(versionResult);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
82
118
|
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
119
|
+
} catch { }
|
|
120
|
+
}
|
|
85
121
|
|
|
86
122
|
return { ...cli, installed: true, version, path: firstPath };
|
|
87
123
|
} catch {
|
|
@@ -94,7 +130,11 @@ export async function detectCLIs(providerLoader?: ProviderLoader): Promise<CLIIn
|
|
|
94
130
|
}
|
|
95
131
|
|
|
96
132
|
/** Detect specific CLI — only probes the one requested provider */
|
|
97
|
-
export async function detectCLI(
|
|
133
|
+
export async function detectCLI(
|
|
134
|
+
cliId: string,
|
|
135
|
+
providerLoader?: ProviderLoader,
|
|
136
|
+
options?: { includeVersion?: boolean },
|
|
137
|
+
): Promise<CLIInfo | null> {
|
|
98
138
|
const resolvedId = providerLoader ? providerLoader.resolveAlias(cliId) : cliId;
|
|
99
139
|
|
|
100
140
|
if (providerLoader) {
|
|
@@ -104,25 +144,28 @@ export async function detectCLI(cliId: string, providerLoader?: ProviderLoader):
|
|
|
104
144
|
const platform = os.platform();
|
|
105
145
|
const whichCmd = platform === 'win32' ? 'where' : 'which';
|
|
106
146
|
try {
|
|
107
|
-
const
|
|
147
|
+
const explicitPath = resolveCommandPath(target.command);
|
|
148
|
+
const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(target.command)}`);
|
|
108
149
|
if (!pathResult) return null;
|
|
109
|
-
const firstPath = pathResult.split('\n')[0];
|
|
150
|
+
const firstPath = explicitPath || pathResult.split('\n')[0];
|
|
110
151
|
let version: string | undefined;
|
|
111
|
-
|
|
152
|
+
if (options?.includeVersion !== false) {
|
|
112
153
|
const versionCommands = [
|
|
154
|
+
`"${firstPath}" --version`,
|
|
155
|
+
`"${firstPath}" -V`,
|
|
156
|
+
`"${firstPath}" -v`,
|
|
113
157
|
target.versionCommand,
|
|
114
|
-
`${target.command} --version`,
|
|
115
|
-
`${target.command} -V`,
|
|
116
|
-
`${target.command} -v`,
|
|
117
158
|
].filter((v): v is string => !!v);
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
159
|
+
try {
|
|
160
|
+
for (const versionCommand of versionCommands) {
|
|
161
|
+
const versionResult = await execAsync(versionCommand, 3000);
|
|
162
|
+
if (versionResult) {
|
|
163
|
+
version = parseVersion(versionResult);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
123
166
|
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
167
|
+
} catch { }
|
|
168
|
+
}
|
|
126
169
|
return { ...target, installed: true, version, path: firstPath };
|
|
127
170
|
} catch {
|
|
128
171
|
return null;
|
|
@@ -131,6 +174,6 @@ export async function detectCLI(cliId: string, providerLoader?: ProviderLoader):
|
|
|
131
174
|
}
|
|
132
175
|
|
|
133
176
|
// Fallback: full scan for unknown provider IDs
|
|
134
|
-
const all = await detectCLIs(providerLoader);
|
|
177
|
+
const all = await detectCLIs(providerLoader, options);
|
|
135
178
|
return all.find((c) => c.id === resolvedId && c.installed) || null;
|
|
136
179
|
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
12
|
import { platform, homedir } from 'os';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import type { ProviderLoader } from '../providers/provider-loader.js';
|
|
13
15
|
|
|
14
16
|
// ─── Types ──────────────────────────────────────
|
|
15
17
|
|
|
@@ -62,9 +64,18 @@ function getMergedDefinitions(): IDEDefinition[] {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
function findCliCommand(command: string): string | null {
|
|
67
|
+
const trimmed = String(command || '').trim();
|
|
68
|
+
if (!trimmed) return null;
|
|
69
|
+
if (path.isAbsolute(trimmed) || trimmed.includes('/') || trimmed.includes('\\') || trimmed.startsWith('~')) {
|
|
70
|
+
const candidate = trimmed.startsWith('~')
|
|
71
|
+
? path.join(homedir(), trimmed.slice(1))
|
|
72
|
+
: trimmed;
|
|
73
|
+
const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(candidate);
|
|
74
|
+
return existsSync(resolved) ? resolved : null;
|
|
75
|
+
}
|
|
65
76
|
try {
|
|
66
77
|
const result = execSync(
|
|
67
|
-
platform() === 'win32' ? `where ${
|
|
78
|
+
platform() === 'win32' ? `where ${trimmed}` : `which ${trimmed}`,
|
|
68
79
|
{ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
69
80
|
).trim();
|
|
70
81
|
return result.split('\n')[0] || null;
|
|
@@ -89,27 +100,29 @@ function getIdeVersion(cliCommand: string): string | null {
|
|
|
89
100
|
function checkPathExists(paths: string[]): string | null {
|
|
90
101
|
const home = homedir();
|
|
91
102
|
for (const p of paths) {
|
|
92
|
-
|
|
103
|
+
const normalized = p.startsWith('~')
|
|
104
|
+
? path.join(home, p.slice(1))
|
|
105
|
+
: p;
|
|
106
|
+
if (normalized.includes('*')) {
|
|
93
107
|
// Wildcard expansion: replace `*` with the current user's home folder name
|
|
94
108
|
// e.g. "C:\Users\*\AppData\..." → "C:\Users\vilmi\AppData\..."
|
|
95
109
|
const username = home.split(/[\\/]/).pop() || '';
|
|
96
|
-
const resolved =
|
|
110
|
+
const resolved = normalized.replace('*', username);
|
|
97
111
|
if (existsSync(resolved)) return resolved;
|
|
98
112
|
} else {
|
|
99
|
-
if (existsSync(
|
|
113
|
+
if (existsSync(normalized)) return normalized;
|
|
100
114
|
}
|
|
101
115
|
}
|
|
102
116
|
return null;
|
|
103
117
|
}
|
|
104
118
|
|
|
105
|
-
export async function detectIDEs(): Promise<IDEInfo[]> {
|
|
119
|
+
export async function detectIDEs(providerLoader?: ProviderLoader): Promise<IDEInfo[]> {
|
|
106
120
|
const os = platform() as 'darwin' | 'win32' | 'linux';
|
|
107
121
|
const results: IDEInfo[] = [];
|
|
108
122
|
|
|
109
123
|
for (const def of getMergedDefinitions()) {
|
|
110
|
-
const cliPath = findCliCommand(def.cli);
|
|
111
|
-
const appPath = checkPathExists(def.paths[os] || []);
|
|
112
|
-
const installed = !!(cliPath || appPath);
|
|
124
|
+
const cliPath = findCliCommand(providerLoader?.getIdeCliCommand(def.id, def.cli) || def.cli);
|
|
125
|
+
const appPath = checkPathExists(providerLoader?.getIdePathCandidates(def.id, def.paths[os] || []) || []);
|
|
113
126
|
|
|
114
127
|
let resolvedCli = cliPath;
|
|
115
128
|
|
|
@@ -136,6 +149,9 @@ export async function detectIDEs(): Promise<IDEInfo[]> {
|
|
|
136
149
|
}
|
|
137
150
|
}
|
|
138
151
|
|
|
152
|
+
const installed = os === 'darwin'
|
|
153
|
+
? !!(resolvedCli || appPath)
|
|
154
|
+
: !!resolvedCli;
|
|
139
155
|
const version = resolvedCli ? getIdeVersion(resolvedCli) : null;
|
|
140
156
|
|
|
141
157
|
results.push({
|
package/src/launch.ts
CHANGED
|
@@ -310,7 +310,7 @@ export async function launchWithCdp(options: LaunchOptions = {}): Promise<Launch
|
|
|
310
310
|
|
|
311
311
|
// 1. IDE determine
|
|
312
312
|
let targetIde: IDEInfo | undefined;
|
|
313
|
-
const ides = await detectIDEs();
|
|
313
|
+
const ides = await detectIDEs(getProviderLoader());
|
|
314
314
|
|
|
315
315
|
if (options.ideId) {
|
|
316
316
|
targetIde = ides.find(i => i.id === options.ideId && i.installed);
|
|
@@ -327,8 +327,9 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
327
327
|
// Find configId for this category
|
|
328
328
|
const opt = this.configOptions.find(c => c.category === category);
|
|
329
329
|
if (!opt) {
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
const message = `[${this.type}] No config option for category: ${category}`;
|
|
331
|
+
this.log.warn(message);
|
|
332
|
+
throw new Error(message);
|
|
332
333
|
}
|
|
333
334
|
|
|
334
335
|
// Static config mode: update selection and restart process
|
|
@@ -343,8 +344,9 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
343
344
|
}
|
|
344
345
|
|
|
345
346
|
if (!this.connection || !this.sessionId) {
|
|
346
|
-
|
|
347
|
-
|
|
347
|
+
const message = `[${this.type}] Cannot set config: no active connection/session`;
|
|
348
|
+
this.log.warn(message);
|
|
349
|
+
throw new Error(message);
|
|
348
350
|
}
|
|
349
351
|
|
|
350
352
|
try {
|
|
@@ -361,7 +363,9 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
361
363
|
if (result?.configOptions) this.parseConfigOptions(result.configOptions);
|
|
362
364
|
this.log.info(`[${this.type}] Config ${category} set to: ${value} | response: ${JSON.stringify(result)?.slice(0, 300)}`);
|
|
363
365
|
} catch (e: any) {
|
|
364
|
-
|
|
366
|
+
const message = e?.message || 'Unknown ACP config error';
|
|
367
|
+
this.log.warn(`[${this.type}] set_config_option failed: ${message}`);
|
|
368
|
+
throw new Error(message);
|
|
365
369
|
}
|
|
366
370
|
}
|
|
367
371
|
|
|
@@ -380,8 +384,9 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
380
384
|
}
|
|
381
385
|
|
|
382
386
|
if (!this.connection || !this.sessionId) {
|
|
383
|
-
|
|
384
|
-
|
|
387
|
+
const message = `[${this.type}] Cannot set mode: no active connection/session`;
|
|
388
|
+
this.log.warn(message);
|
|
389
|
+
throw new Error(message);
|
|
385
390
|
}
|
|
386
391
|
|
|
387
392
|
try {
|
|
@@ -392,7 +397,9 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
392
397
|
this.currentMode = modeId;
|
|
393
398
|
this.log.info(`[${this.type}] Mode set to: ${modeId}`);
|
|
394
399
|
} catch (e: any) {
|
|
395
|
-
|
|
400
|
+
const message = e?.message || 'Unknown ACP mode error';
|
|
401
|
+
this.log.warn(`[${this.type}] set_mode failed: ${message}`);
|
|
402
|
+
throw new Error(message);
|
|
396
403
|
}
|
|
397
404
|
}
|
|
398
405
|
|
|
@@ -447,7 +454,9 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
447
454
|
throw new Error(`[ACP:${this.type}] No spawn config defined`);
|
|
448
455
|
}
|
|
449
456
|
|
|
450
|
-
const command =
|
|
457
|
+
const command = typeof this.settings.executablePath === 'string' && this.settings.executablePath.trim()
|
|
458
|
+
? this.settings.executablePath.trim()
|
|
459
|
+
: spawnConfig.command;
|
|
451
460
|
// Static config: create args via spawnArgBuilder (when provider defines it)
|
|
452
461
|
let baseArgs = spawnConfig.args || [];
|
|
453
462
|
if (this.provider.spawnArgBuilder && Object.keys(this.selectedConfig).length > 0) {
|
|
@@ -822,7 +831,7 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
822
831
|
|
|
823
832
|
private permissionResolvers: ((approved: boolean) => void)[] = [];
|
|
824
833
|
|
|
825
|
-
|
|
834
|
+
async resolvePermission(approved: boolean): Promise<void> {
|
|
826
835
|
const resolver = this.permissionResolvers.shift();
|
|
827
836
|
if (resolver) {
|
|
828
837
|
resolver(approved);
|
|
@@ -60,6 +60,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
60
60
|
private historyWriter: ChatHistoryWriter;
|
|
61
61
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
62
62
|
readonly instanceId: string;
|
|
63
|
+
private suppressIdleHistoryReplay = false;
|
|
63
64
|
|
|
64
65
|
private presentationMode: 'terminal' | 'chat';
|
|
65
66
|
private providerSessionId?: string;
|
|
@@ -135,7 +136,15 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
135
136
|
await this.adapter.spawn();
|
|
136
137
|
this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
|
|
137
138
|
if (this.providerSessionId) {
|
|
139
|
+
this.historyWriter.compactHistorySession(this.type, this.providerSessionId);
|
|
138
140
|
const restoredHistory = readChatHistory(this.type, 0, 200, this.providerSessionId);
|
|
141
|
+
this.historyWriter.seedSessionHistory(
|
|
142
|
+
this.type,
|
|
143
|
+
restoredHistory.messages,
|
|
144
|
+
this.providerSessionId,
|
|
145
|
+
this.instanceId,
|
|
146
|
+
);
|
|
147
|
+
this.suppressIdleHistoryReplay = restoredHistory.messages.length > 0;
|
|
139
148
|
if (restoredHistory.messages.length > 0) {
|
|
140
149
|
this.adapter.seedCommittedMessages(
|
|
141
150
|
restoredHistory.messages.map((message) => ({
|
|
@@ -184,7 +193,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
184
193
|
} else if (this.type === 'codex-cli') {
|
|
185
194
|
probedSessionId = this.probeSessionIdFromConfig({
|
|
186
195
|
dbPath: '~/.codex/state_5.sqlite',
|
|
187
|
-
query: 'select id from threads where cwd in ({dirs}) and
|
|
196
|
+
query: 'select id from threads where cwd in ({dirs}) and updated_at >= ? and archived = 0 order by updated_at desc limit 1',
|
|
188
197
|
timestampFormat: 'unix_s',
|
|
189
198
|
});
|
|
190
199
|
} else if (this.type === 'goose-cli') {
|
|
@@ -260,6 +269,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
260
269
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
261
270
|
|
|
262
271
|
if (parsedMessages.length > 0) {
|
|
272
|
+
const shouldSkipReplayPersist =
|
|
273
|
+
this.suppressIdleHistoryReplay
|
|
274
|
+
&& adapterStatus.status === 'idle'
|
|
275
|
+
&& parsedStatus?.status === 'idle';
|
|
263
276
|
let messagesToSave = parsedMessages;
|
|
264
277
|
if ((parsedStatus?.status === 'generating' || parsedStatus?.status === 'long_generating')) {
|
|
265
278
|
const lastIdx = messagesToSave.length - 1;
|
|
@@ -267,7 +280,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
267
280
|
messagesToSave = messagesToSave.slice(0, lastIdx);
|
|
268
281
|
}
|
|
269
282
|
}
|
|
270
|
-
if (messagesToSave.length > 0) {
|
|
283
|
+
if (!shouldSkipReplayPersist && messagesToSave.length > 0) {
|
|
271
284
|
this.historyWriter.appendNewMessages(
|
|
272
285
|
this.type,
|
|
273
286
|
messagesToSave,
|
|
@@ -374,6 +387,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
374
387
|
if (newStatus !== this.lastStatus) {
|
|
375
388
|
LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
|
|
376
389
|
if (this.lastStatus === 'idle' && newStatus === 'generating') {
|
|
390
|
+
this.suppressIdleHistoryReplay = false;
|
|
377
391
|
// Cancel any pending completed event (multi-step: idle→generating resume)
|
|
378
392
|
if (this.completedDebouncePending) {
|
|
379
393
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating)`);
|
|
@@ -394,6 +408,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
394
408
|
this.generatingDebounceTimer = null;
|
|
395
409
|
}, 1000);
|
|
396
410
|
} else if (newStatus === 'waiting_approval') {
|
|
411
|
+
this.suppressIdleHistoryReplay = false;
|
|
397
412
|
// Flush pending generating_started if debounce still pending
|
|
398
413
|
if (this.generatingDebouncePending) {
|
|
399
414
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
@@ -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
|
|
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
|
|
892
|
-
|
|
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
|
|
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
|
|
934
|
-
if (!provider?.settings) return {};
|
|
1014
|
+
const settings = this.getSettingsSchema(type);
|
|
935
1015
|
const result: Record<string, any> = {};
|
|
936
|
-
for (const [key
|
|
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
|
|
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
|
/**
|
package/src/shared-types.ts
CHANGED
|
@@ -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) */
|