@adhdev/daemon-core 0.9.82-rc.143 → 0.9.82-rc.145
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/boot/process-hardening.d.ts +50 -0
- package/dist/cli-adapters/cli-script-runner.d.ts +73 -1
- package/dist/cli-adapters/provider-cli-adapter.d.ts +17 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +6 -0
- package/dist/commands/handler.d.ts +66 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2943 -435
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2957 -456
- package/dist/index.mjs.map +1 -1
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/providers/contracts.d.ts +31 -5
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/index.d.ts +11 -0
- package/dist/providers/provider-loader.d.ts +19 -1
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +64 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/package.json +4 -2
- package/src/boot/daemon-lifecycle.ts +14 -10
- package/src/boot/process-hardening.ts +89 -0
- package/src/cli-adapters/cli-script-runner.ts +289 -13
- package/src/cli-adapters/cli-state-engine.ts +8 -5
- package/src/cli-adapters/provider-cli-adapter.ts +36 -2
- package/src/cli-adapters/provider-cli-shared.ts +6 -0
- package/src/commands/chat-commands.ts +35 -14
- package/src/commands/cli-manager.ts +39 -0
- package/src/commands/handler.ts +539 -1
- package/src/commands/router.ts +2 -1
- package/src/index.ts +27 -0
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/providers/cli-provider-instance.ts +18 -3
- package/src/providers/contracts.ts +33 -5
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +419 -0
- package/src/providers/native-history/index.ts +23 -0
- package/src/providers/provider-loader.ts +263 -17
- package/src/providers/provider-schema.ts +21 -15
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +245 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +444 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +119 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +210 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
|
@@ -33,6 +33,26 @@ import { validateProviderDefinition } from './provider-schema.js';
|
|
|
33
33
|
import type { ProviderSourceMode } from '../config/config.js';
|
|
34
34
|
import type { ProviderSourceConfigSnapshot, ProviderUserDirSource } from '../config/provider-source-config.js';
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Adds a provider-script root to the require whitelist. Wrapped in a
|
|
38
|
+
* try/catch + null check so a loader hot-path can't crash on a path
|
|
39
|
+
* that doesn't exist yet or one the whitelist hook rejects.
|
|
40
|
+
*
|
|
41
|
+
* The require-whitelist module is loaded lazily on first call. Eagerly
|
|
42
|
+
* top-level importing it pulls `node:fs.realpathSync.native` into
|
|
43
|
+
* module evaluation, which breaks unit tests that partially mock `fs`
|
|
44
|
+
* (e.g. test/commands/get-logs-incremental.test.ts mocks only
|
|
45
|
+
* existsSync + readFileSync). Lazy load keeps that mock surface valid.
|
|
46
|
+
*/
|
|
47
|
+
function registerProviderScriptRootSafely(root: string | null | undefined): void {
|
|
48
|
+
if (!root || typeof root !== 'string') return;
|
|
49
|
+
try {
|
|
50
|
+
const { registerProviderScriptRoot } =
|
|
51
|
+
require('./sdk/v1/sandbox/require-whitelist.js') as typeof import('./sdk/v1/sandbox/require-whitelist.js');
|
|
52
|
+
registerProviderScriptRoot(root);
|
|
53
|
+
} catch { /* boot-time only — swallow */ }
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
interface ProviderAvailabilityState {
|
|
37
57
|
installed: boolean;
|
|
38
58
|
detectedPath: string | null;
|
|
@@ -92,7 +112,9 @@ export class ProviderLoader {
|
|
|
92
112
|
}
|
|
93
113
|
|
|
94
114
|
private static readonly GITHUB_TARBALL_URL = 'https://github.com/vilmire/adhdev-providers/archive/refs/heads/main.tar.gz';
|
|
115
|
+
private static readonly REGISTRY_BASE_URL = 'https://api.adhf.dev/api/v1/registry';
|
|
95
116
|
private static readonly META_FILE = '.meta.json';
|
|
117
|
+
private static readonly REGISTRY_META_FILE = '.registry-meta.json';
|
|
96
118
|
private static readonly REPO_PROVIDER_DIRNAME = 'adhdev-providers';
|
|
97
119
|
private static readonly SIBLING_MARKER_FILE = '.adhdev-provider-root';
|
|
98
120
|
private static readonly SIBLING_ENV_VAR = 'ADHDEV_USE_SIBLING_PROVIDERS';
|
|
@@ -224,7 +246,12 @@ export class ProviderLoader {
|
|
|
224
246
|
* Highest-priority editable overrides come first.
|
|
225
247
|
*/
|
|
226
248
|
getProviderRoots(): string[] {
|
|
227
|
-
|
|
249
|
+
// Order matters: user customs > marketplace installs > upstream auto-sync.
|
|
250
|
+
// findProviderDirInternal walks this list in order to locate the provider
|
|
251
|
+
// dir containing the scripts/, so marketplace must be included here even
|
|
252
|
+
// though loadAll() also reads it directly.
|
|
253
|
+
const marketplaceDir = path.join(os.homedir(), '.adhdev', 'marketplace');
|
|
254
|
+
return [this.userDir, marketplaceDir, this.upstreamDir];
|
|
228
255
|
}
|
|
229
256
|
|
|
230
257
|
getSourceConfig(): ProviderSourceConfigSnapshot {
|
|
@@ -336,7 +363,19 @@ export class ProviderLoader {
|
|
|
336
363
|
this.log('Upstream loading disabled (sourceMode=no-upstream)');
|
|
337
364
|
}
|
|
338
365
|
|
|
339
|
-
// 2. Load
|
|
366
|
+
// 2. Load marketplace installs from ~/.adhdev/marketplace/ (overrides upstream,
|
|
367
|
+
// but is itself overridden by user customs in step 3). These are providers the
|
|
368
|
+
// user explicitly installed via the Marketplace UI. They are NOT touched by
|
|
369
|
+
// upstream sync.
|
|
370
|
+
const marketplaceDir = path.join(os.homedir(), '.adhdev', 'marketplace');
|
|
371
|
+
if (fs.existsSync(marketplaceDir)) {
|
|
372
|
+
const marketplaceCount = this.loadDir(marketplaceDir);
|
|
373
|
+
if (marketplaceCount > 0) {
|
|
374
|
+
this.log(`Loaded ${marketplaceCount} marketplace-installed providers`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 3. Load user custom (excluding .upstream — highest priority, never auto-updated)
|
|
340
379
|
if (fs.existsSync(this.userDir)) {
|
|
341
380
|
const userCount = this.loadDir(this.userDir, ['.upstream']);
|
|
342
381
|
if (userCount > 0) {
|
|
@@ -988,7 +1027,12 @@ export class ProviderLoader {
|
|
|
988
1027
|
}
|
|
989
1028
|
|
|
990
1029
|
// 3. Composite override (OS + version)
|
|
991
|
-
|
|
1030
|
+
// Legacy shape: base.overrides is an Array<{ when: {os,version}, scripts }>.
|
|
1031
|
+
// v1 manifests (Phase 3-4) repurposed `overrides` as an object map of
|
|
1032
|
+
// capability overrides (e.g. { detectStatus: { path, schema } }), which is
|
|
1033
|
+
// consumed by the SDK builders, not by this resolver. Only iterate when
|
|
1034
|
+
// the field is in the legacy array shape.
|
|
1035
|
+
if (Array.isArray(base.overrides)) {
|
|
992
1036
|
for (const override of base.overrides) {
|
|
993
1037
|
const osMatch = !override.when.os || override.when.os === currentOs;
|
|
994
1038
|
const verMatch = !override.when.version || (currentVersion && this.matchesVersion(currentVersion, override.when.version));
|
|
@@ -996,6 +1040,40 @@ export class ProviderLoader {
|
|
|
996
1040
|
resolved.scripts = { ...resolved.scripts, ...override.scripts };
|
|
997
1041
|
}
|
|
998
1042
|
}
|
|
1043
|
+
} else if (base.overrides && typeof base.overrides === 'object') {
|
|
1044
|
+
// v1 manifest shape: { detectStatus: { path }, parseSession: { path }, ... }
|
|
1045
|
+
// Each script name maps to a path inside the provider directory. We load
|
|
1046
|
+
// the file and merge its export(s) into resolved.scripts. Lets a
|
|
1047
|
+
// provider override a single primitive (e.g. just detectStatus) while
|
|
1048
|
+
// letting the SDK synthesize the rest from the tui block.
|
|
1049
|
+
const providerDir = this.findProviderDirInternal(base.type);
|
|
1050
|
+
if (providerDir) {
|
|
1051
|
+
for (const [scriptName, override] of Object.entries(base.overrides as Record<string, any>)) {
|
|
1052
|
+
if (!override || typeof override.path !== 'string') continue;
|
|
1053
|
+
const fullPath = path.join(providerDir, override.path);
|
|
1054
|
+
if (!fs.existsSync(fullPath)) {
|
|
1055
|
+
this.log(` [overrides] ${base.type}: ${scriptName} path not found: ${fullPath}`);
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
// Override scripts go through the same whitelist gate as the
|
|
1060
|
+
// main scripts dir. Use the provider parent root so a v1
|
|
1061
|
+
// override can still require ../_shared helpers.
|
|
1062
|
+
registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
|
|
1063
|
+
delete require.cache[require.resolve(fullPath)];
|
|
1064
|
+
const fn = require(fullPath);
|
|
1065
|
+
const target = typeof fn === 'function' ? fn : (fn && fn[scriptName]);
|
|
1066
|
+
if (typeof target === 'function') {
|
|
1067
|
+
resolved.scripts = { ...resolved.scripts, [scriptName]: target } as any;
|
|
1068
|
+
this.log(` [overrides] ${base.type}: ${scriptName} loaded from ${override.path}`);
|
|
1069
|
+
} else {
|
|
1070
|
+
this.log(` [overrides] ${base.type}: ${scriptName} export missing in ${override.path}`);
|
|
1071
|
+
}
|
|
1072
|
+
} catch (e: any) {
|
|
1073
|
+
this.log(` [overrides] ${base.type}: ${scriptName} require failed: ${e?.message || e}`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
999
1077
|
}
|
|
1000
1078
|
|
|
1001
1079
|
if ((resolved.category === 'cli' || resolved.category === 'acp') && resolved.spawn?.command) {
|
|
@@ -1026,6 +1104,13 @@ export class ProviderLoader {
|
|
|
1026
1104
|
return null;
|
|
1027
1105
|
}
|
|
1028
1106
|
|
|
1107
|
+
// Register the provider's *parent root* (e.g. .../adhdev-providers/) so
|
|
1108
|
+
// the require whitelist gates every script + every _shared helper this
|
|
1109
|
+
// provider may reach. Picking the grandparent (one above the category
|
|
1110
|
+
// dir `cli/`) lets sibling helpers in `_shared` resolve while still
|
|
1111
|
+
// blocking `../../etc/...` escapes. Idempotent.
|
|
1112
|
+
registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
|
|
1113
|
+
|
|
1029
1114
|
// Return cached scripts if available (cleared on reload/watch)
|
|
1030
1115
|
const cached = this.scriptsCache.get(dir);
|
|
1031
1116
|
if (cached) return cached;
|
|
@@ -1128,6 +1213,109 @@ export class ProviderLoader {
|
|
|
1128
1213
|
*
|
|
1129
1214
|
* @returns Whether an update occurred
|
|
1130
1215
|
*/
|
|
1216
|
+
/**
|
|
1217
|
+
* Sync providers from the ADHDev registry (registry.adhf.dev).
|
|
1218
|
+
*
|
|
1219
|
+
* Downloads only providers whose server checksum differs from the locally
|
|
1220
|
+
* cached checksum. Falls back gracefully to the GitHub tarball path if the
|
|
1221
|
+
* registry is unreachable or returns an unexpected response.
|
|
1222
|
+
*
|
|
1223
|
+
* Returns `{ updated: true }` when at least one provider file changed on disk,
|
|
1224
|
+
* `{ updated: false }` when everything is already current, or
|
|
1225
|
+
* `{ updated: false, error }` when the registry couldn't be reached and we
|
|
1226
|
+
* should proceed to the GitHub tarball fallback.
|
|
1227
|
+
*/
|
|
1228
|
+
async fetchFromRegistry(): Promise<{ updated: boolean; error?: string }> {
|
|
1229
|
+
if (this.disableUpstream) {
|
|
1230
|
+
this.log('Registry sync skipped (sourceMode=no-upstream)');
|
|
1231
|
+
return { updated: false };
|
|
1232
|
+
}
|
|
1233
|
+
this.log(`Registry sync starting (${ProviderLoader.REGISTRY_BASE_URL})...`);
|
|
1234
|
+
|
|
1235
|
+
const https = require('https') as typeof import('https');
|
|
1236
|
+
const regMetaPath = path.join(this.upstreamDir, ProviderLoader.REGISTRY_META_FILE);
|
|
1237
|
+
|
|
1238
|
+
// Load cached checksums
|
|
1239
|
+
let cachedChecksums: Record<string, string> = {};
|
|
1240
|
+
try {
|
|
1241
|
+
if (fs.existsSync(regMetaPath)) {
|
|
1242
|
+
cachedChecksums = JSON.parse(fs.readFileSync(regMetaPath, 'utf-8')).checksums ?? {};
|
|
1243
|
+
}
|
|
1244
|
+
} catch { }
|
|
1245
|
+
|
|
1246
|
+
try {
|
|
1247
|
+
// 1. Fetch provider list
|
|
1248
|
+
const listUrl = `${ProviderLoader.REGISTRY_BASE_URL}/providers`;
|
|
1249
|
+
const listBody = await new Promise<string>((resolve, reject) => {
|
|
1250
|
+
const req = https.get(listUrl, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: 10000 }, (res) => {
|
|
1251
|
+
if (res.statusCode !== 200) { reject(new Error(`registry list HTTP ${res.statusCode}`)); return; }
|
|
1252
|
+
const chunks: Buffer[] = [];
|
|
1253
|
+
res.on('data', (c: Buffer) => chunks.push(c));
|
|
1254
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
1255
|
+
});
|
|
1256
|
+
req.on('error', reject);
|
|
1257
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('registry list timeout')); });
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
const list = JSON.parse(listBody) as { providers: Array<{ type: string; category: string; checksum: string; version: string }> };
|
|
1261
|
+
if (!Array.isArray(list.providers)) throw new Error('unexpected registry response shape');
|
|
1262
|
+
|
|
1263
|
+
let updatedCount = 0;
|
|
1264
|
+
|
|
1265
|
+
for (const entry of list.providers) {
|
|
1266
|
+
const { type, category, checksum, version } = entry;
|
|
1267
|
+
const cacheKey = `${category}/${type}`;
|
|
1268
|
+
if (cachedChecksums[cacheKey] === checksum) continue; // already current
|
|
1269
|
+
|
|
1270
|
+
// Download this provider's manifest
|
|
1271
|
+
const dlUrl = `${ProviderLoader.REGISTRY_BASE_URL}/providers/${type}/${version}/download`;
|
|
1272
|
+
const manifestBody = await new Promise<string>((resolve, reject) => {
|
|
1273
|
+
const req = https.get(dlUrl, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: 30000 }, (res) => {
|
|
1274
|
+
if (res.statusCode !== 200) { reject(new Error(`registry download HTTP ${res.statusCode} for ${type}@${version}`)); return; }
|
|
1275
|
+
const chunks: Buffer[] = [];
|
|
1276
|
+
res.on('data', (c: Buffer) => chunks.push(c));
|
|
1277
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
1278
|
+
});
|
|
1279
|
+
req.on('error', reject);
|
|
1280
|
+
req.on('timeout', () => { req.destroy(); reject(new Error(`download timeout for ${type}`)); });
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
// Verify checksum
|
|
1284
|
+
const actualChecksum = await new Promise<string>((resolve) => {
|
|
1285
|
+
const crypto = require('crypto') as typeof import('crypto');
|
|
1286
|
+
resolve(crypto.createHash('sha256').update(manifestBody, 'utf-8').digest('hex'));
|
|
1287
|
+
});
|
|
1288
|
+
if (actualChecksum !== checksum) {
|
|
1289
|
+
this.log(`⚠ Registry checksum mismatch for ${type}@${version} — skipping`);
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Write to upstream dir
|
|
1294
|
+
const providerDir = path.join(this.upstreamDir, category, type);
|
|
1295
|
+
fs.mkdirSync(providerDir, { recursive: true });
|
|
1296
|
+
fs.writeFileSync(path.join(providerDir, 'provider.json'), manifestBody, 'utf-8');
|
|
1297
|
+
|
|
1298
|
+
cachedChecksums[cacheKey] = checksum;
|
|
1299
|
+
updatedCount++;
|
|
1300
|
+
this.log(`✓ Registry updated: ${category}/${type}@${version}`);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Persist updated checksums
|
|
1304
|
+
fs.mkdirSync(this.upstreamDir, { recursive: true });
|
|
1305
|
+
fs.writeFileSync(regMetaPath, JSON.stringify({
|
|
1306
|
+
checksums: cachedChecksums,
|
|
1307
|
+
syncedAt: new Date().toISOString(),
|
|
1308
|
+
providerCount: list.providers.length,
|
|
1309
|
+
}, null, 2));
|
|
1310
|
+
|
|
1311
|
+
this.log(`Registry sync complete: ${list.providers.length} providers, ${updatedCount} updated`);
|
|
1312
|
+
return { updated: updatedCount > 0 };
|
|
1313
|
+
} catch (e: any) {
|
|
1314
|
+
this.log(`⚠ Registry sync failed (falling back to GitHub tarball): ${e?.message}`);
|
|
1315
|
+
return { updated: false, error: e?.message };
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1131
1319
|
async fetchLatest(): Promise<{ updated: boolean; error?: string }> {
|
|
1132
1320
|
if (this.disableUpstream) {
|
|
1133
1321
|
this.log('Upstream fetch skipped (sourceMode=no-upstream)');
|
|
@@ -1321,15 +1509,17 @@ export class ProviderLoader {
|
|
|
1321
1509
|
} catch { }
|
|
1322
1510
|
}
|
|
1323
1511
|
|
|
1324
|
-
/** Count provider files (provider.
|
|
1512
|
+
/** Count provider files (provider.v1.json or provider.json — at most one per dir). */
|
|
1325
1513
|
private countProviders(dir: string): number {
|
|
1326
1514
|
if (!fs.existsSync(dir)) return 0;
|
|
1327
1515
|
let count = 0;
|
|
1328
1516
|
const scan = (d: string) => {
|
|
1329
1517
|
try {
|
|
1330
|
-
|
|
1518
|
+
const entries = fs.readdirSync(d, { withFileTypes: true });
|
|
1519
|
+
const hasManifest = entries.some(e => e.name === 'provider.v1.json' || e.name === 'provider.json');
|
|
1520
|
+
if (hasManifest) count++;
|
|
1521
|
+
for (const entry of entries) {
|
|
1331
1522
|
if (entry.isDirectory()) scan(path.join(d, entry.name));
|
|
1332
|
-
else if (entry.name === 'provider.json') count++;
|
|
1333
1523
|
}
|
|
1334
1524
|
} catch { }
|
|
1335
1525
|
};
|
|
@@ -1601,23 +1791,32 @@ export class ProviderLoader {
|
|
|
1601
1791
|
const cat = provider.category;
|
|
1602
1792
|
|
|
1603
1793
|
const searchRoots = this.getProviderRoots();
|
|
1794
|
+
const hasManifest = (dir: string) =>
|
|
1795
|
+
fs.existsSync(path.join(dir, 'provider.v1.json')) || fs.existsSync(path.join(dir, 'provider.json'));
|
|
1796
|
+
const readManifestType = (dir: string): string | null => {
|
|
1797
|
+
for (const file of ['provider.v1.json', 'provider.json']) {
|
|
1798
|
+
const p = path.join(dir, file);
|
|
1799
|
+
if (!fs.existsSync(p)) continue;
|
|
1800
|
+
try {
|
|
1801
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
1802
|
+
if (typeof data?.type === 'string') return data.type;
|
|
1803
|
+
} catch { /* skip */ }
|
|
1804
|
+
}
|
|
1805
|
+
return null;
|
|
1806
|
+
};
|
|
1604
1807
|
for (const root of searchRoots) {
|
|
1605
1808
|
if (!fs.existsSync(root)) continue;
|
|
1606
1809
|
const candidate = this.getProviderDir(root, cat, type);
|
|
1607
|
-
if (
|
|
1810
|
+
if (hasManifest(candidate)) return candidate;
|
|
1608
1811
|
// Scan category dir for type match
|
|
1609
1812
|
const catDir = path.join(root, cat);
|
|
1610
1813
|
if (fs.existsSync(catDir)) {
|
|
1611
1814
|
try {
|
|
1612
1815
|
for (const entry of fs.readdirSync(catDir, { withFileTypes: true })) {
|
|
1613
1816
|
if (!entry.isDirectory()) continue;
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
1618
|
-
if (data.type === type) return path.join(catDir, entry.name);
|
|
1619
|
-
} catch { /* skip */ }
|
|
1620
|
-
}
|
|
1817
|
+
const entryDir = path.join(catDir, entry.name);
|
|
1818
|
+
const manifestType = readManifestType(entryDir);
|
|
1819
|
+
if (manifestType === type) return entryDir;
|
|
1621
1820
|
}
|
|
1622
1821
|
} catch { /* skip */ }
|
|
1623
1822
|
}
|
|
@@ -1710,17 +1909,44 @@ export class ProviderLoader {
|
|
|
1710
1909
|
return;
|
|
1711
1910
|
}
|
|
1712
1911
|
|
|
1713
|
-
//
|
|
1912
|
+
// v1-first manifest selection. provider.v1.json (the SDK-shape
|
|
1913
|
+
// manifest with `overrides`, `tui`, `source`, `canonicalHistory`)
|
|
1914
|
+
// wins over provider.json (legacy). Without this branch the v1
|
|
1915
|
+
// file is silently ignored — that's how the codex-cli `overrides`
|
|
1916
|
+
// path and the tui-block builders went un-honored for the first
|
|
1917
|
+
// pass of SDK rollout.
|
|
1918
|
+
const hasV1 = entries.some(e => e.name === 'provider.v1.json');
|
|
1714
1919
|
const hasJson = entries.some(e => e.name === 'provider.json');
|
|
1715
1920
|
|
|
1716
|
-
if (hasJson) {
|
|
1717
|
-
const
|
|
1921
|
+
if (hasV1 || hasJson) {
|
|
1922
|
+
const manifestFile = hasV1 ? 'provider.v1.json' : 'provider.json';
|
|
1923
|
+
const jsonPath = path.join(d, manifestFile);
|
|
1718
1924
|
try {
|
|
1719
1925
|
const raw = fs.readFileSync(jsonPath, 'utf-8');
|
|
1720
1926
|
const mod = JSON.parse(raw) as Omit<ProviderModule, 'extensionIdPattern'> & {
|
|
1721
1927
|
extensionIdPattern?: RegExp | string;
|
|
1722
1928
|
};
|
|
1723
1929
|
|
|
1930
|
+
// Validate v1 manifests against the SDK schema. Failures are
|
|
1931
|
+
// surfaced as a single warning line with all issues attached
|
|
1932
|
+
// so manifest authors don't need to guess which field is wrong.
|
|
1933
|
+
// Loading still proceeds — bricking the daemon on a single
|
|
1934
|
+
// bad field would be worse than running with a known warning.
|
|
1935
|
+
if (hasV1 && mod?.category === 'cli') {
|
|
1936
|
+
try {
|
|
1937
|
+
const { validateCliProviderManifest, formatManifestValidationIssues } =
|
|
1938
|
+
require('./sdk/v1/validators/manifest.js') as typeof import('./sdk/v1/validators/manifest.js');
|
|
1939
|
+
const validation = validateCliProviderManifest(mod);
|
|
1940
|
+
if (!validation.ok) {
|
|
1941
|
+
this.log(`⚠ ${jsonPath}: schema validation failed:\n${formatManifestValidationIssues(validation.issues)}`);
|
|
1942
|
+
}
|
|
1943
|
+
} catch (e: any) {
|
|
1944
|
+
// Validator load failed — log once and continue so a
|
|
1945
|
+
// broken validator can't take down provider loading.
|
|
1946
|
+
this.log(`⚠ ${jsonPath}: validator unavailable: ${e?.message || e}`);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1724
1950
|
// Restore RegExp fields from JSON (extensionIdPattern)
|
|
1725
1951
|
if (typeof mod.extensionIdPattern === 'string') {
|
|
1726
1952
|
const flags = mod.extensionIdPattern_flags || '';
|
|
@@ -1732,6 +1958,22 @@ export class ProviderLoader {
|
|
|
1732
1958
|
...(extensionIdPattern instanceof RegExp ? { extensionIdPattern } : {}),
|
|
1733
1959
|
};
|
|
1734
1960
|
|
|
1961
|
+
// v1 manifests use `nativeHistory` as the canonical field name.
|
|
1962
|
+
// Legacy v0 manifests use `canonicalHistory`. The daemon's
|
|
1963
|
+
// runtime + downstream code reads `provider.nativeHistory`, so
|
|
1964
|
+
// for legacy manifests we copy `canonicalHistory` into
|
|
1965
|
+
// `nativeHistory` here. We also keep `canonicalHistory`
|
|
1966
|
+
// populated in both directions (deprecated alias) so any
|
|
1967
|
+
// external consumers still reading the old name keep working
|
|
1968
|
+
// during the one-release deprecation window.
|
|
1969
|
+
const nh = (normalizedProvider as any).nativeHistory;
|
|
1970
|
+
const ch = (normalizedProvider as any).canonicalHistory;
|
|
1971
|
+
if (nh && !ch) {
|
|
1972
|
+
(normalizedProvider as any).canonicalHistory = nh;
|
|
1973
|
+
} else if (ch && !nh) {
|
|
1974
|
+
(normalizedProvider as any).nativeHistory = ch;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1735
1977
|
const validation = validateProviderDefinition(normalizedProvider);
|
|
1736
1978
|
for (const warning of validation.warnings) {
|
|
1737
1979
|
this.log(`⚠ ${jsonPath}: ${warning}`);
|
|
@@ -1745,6 +1987,10 @@ export class ProviderLoader {
|
|
|
1745
1987
|
const scriptsPath = path.join(d, 'scripts.js');
|
|
1746
1988
|
if (!hasCompatibility && fs.existsSync(scriptsPath)) {
|
|
1747
1989
|
try {
|
|
1990
|
+
// Gate the IDE/extension scripts.js (legacy single-file
|
|
1991
|
+
// format) under the same whitelist. `d` here is the
|
|
1992
|
+
// provider dir; its grandparent contains _shared.
|
|
1993
|
+
registerProviderScriptRootSafely(path.dirname(path.dirname(d)));
|
|
1748
1994
|
delete require.cache[require.resolve(scriptsPath)];
|
|
1749
1995
|
const scripts = require(scriptsPath) as Partial<ProviderScripts>;
|
|
1750
1996
|
normalizedProvider.scripts = scripts;
|
|
@@ -37,6 +37,12 @@ const KNOWN_PROVIDER_FIELDS = new Set<string>([
|
|
|
37
37
|
'approvalPositiveHints',
|
|
38
38
|
'sessionIdPattern',
|
|
39
39
|
'historyBehavior',
|
|
40
|
+
// v1 canonical field name. The loader fills this in for legacy v0
|
|
41
|
+
// manifests that ship the old `canonicalHistory` spelling.
|
|
42
|
+
'nativeHistory',
|
|
43
|
+
// Deprecated v0 alias for `nativeHistory`. Kept in the allowed-keys
|
|
44
|
+
// list so legacy provider.json manifests don't trip the unknown-key
|
|
45
|
+
// warning during the one-release deprecation window.
|
|
40
46
|
'canonicalHistory',
|
|
41
47
|
'autoFixProfile',
|
|
42
48
|
'ideLevelScripts',
|
|
@@ -130,7 +136,7 @@ export function validateProviderDefinition(raw: unknown): ProviderValidationResu
|
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
validateCapabilities(provider as unknown as ProviderModule, controls, errors)
|
|
133
|
-
|
|
139
|
+
validateNativeHistory(provider.nativeHistory, errors)
|
|
134
140
|
validateMeshCoordinator(provider.meshCoordinator, errors)
|
|
135
141
|
|
|
136
142
|
for (const control of controls) {
|
|
@@ -230,43 +236,43 @@ function validateCapabilities(provider: ProviderModule, controls: ProviderContro
|
|
|
230
236
|
}
|
|
231
237
|
}
|
|
232
238
|
|
|
233
|
-
function
|
|
239
|
+
function validateNativeHistory(raw: unknown, errors: string[]): void {
|
|
234
240
|
if (raw === undefined) return
|
|
235
241
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
236
|
-
errors.push('
|
|
242
|
+
errors.push('nativeHistory must be an object')
|
|
237
243
|
return
|
|
238
244
|
}
|
|
239
245
|
|
|
240
|
-
const
|
|
241
|
-
const format =
|
|
246
|
+
const nativeHistory = raw as Record<string, unknown>
|
|
247
|
+
const format = nativeHistory.format
|
|
242
248
|
if (format !== undefined && (typeof format !== 'string' || !format.trim())) {
|
|
243
|
-
errors.push('
|
|
249
|
+
errors.push('nativeHistory.format must be a non-empty string when provided')
|
|
244
250
|
}
|
|
245
251
|
|
|
246
|
-
const watchPath =
|
|
252
|
+
const watchPath = nativeHistory.watchPath
|
|
247
253
|
if (watchPath !== undefined && (typeof watchPath !== 'string' || !watchPath.trim())) {
|
|
248
|
-
errors.push('
|
|
254
|
+
errors.push('nativeHistory.watchPath must be a non-empty string when provided')
|
|
249
255
|
}
|
|
250
256
|
|
|
251
|
-
const mode =
|
|
257
|
+
const mode = nativeHistory.mode
|
|
252
258
|
if (mode !== undefined && !['native-source', 'materialized-mirror', 'disabled'].includes(String(mode))) {
|
|
253
|
-
errors.push('
|
|
259
|
+
errors.push('nativeHistory.mode must be one of: native-source, materialized-mirror, disabled')
|
|
254
260
|
}
|
|
255
261
|
|
|
256
262
|
// Chat transcript contract version (transcript-v2.ts). Absent → treated as
|
|
257
263
|
// v1 by readDeclaredChatContractVersion(). A2 will reject unrecognised
|
|
258
264
|
// values at load time; A1 only validates the field shape.
|
|
259
|
-
const chatContractVersion =
|
|
265
|
+
const chatContractVersion = nativeHistory.contractVersion
|
|
260
266
|
if (chatContractVersion !== undefined
|
|
261
267
|
&& chatContractVersion !== '1.0'
|
|
262
268
|
&& chatContractVersion !== '2.0') {
|
|
263
|
-
errors.push(`
|
|
269
|
+
errors.push(`nativeHistory.contractVersion must be '1.0' or '2.0' when provided (got ${JSON.stringify(chatContractVersion)})`)
|
|
264
270
|
}
|
|
265
271
|
|
|
266
|
-
const scripts =
|
|
272
|
+
const scripts = nativeHistory.scripts
|
|
267
273
|
if (scripts === undefined) return
|
|
268
274
|
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) {
|
|
269
|
-
errors.push('
|
|
275
|
+
errors.push('nativeHistory.scripts must be an object')
|
|
270
276
|
return
|
|
271
277
|
}
|
|
272
278
|
|
|
@@ -274,7 +280,7 @@ function validateCanonicalHistory(raw: unknown, errors: string[]): void {
|
|
|
274
280
|
for (const key of ['readSession', 'listSessions']) {
|
|
275
281
|
const value = scriptConfig[key]
|
|
276
282
|
if (typeof value !== 'string' || !value.trim()) {
|
|
277
|
-
errors.push(`
|
|
283
|
+
errors.push(`nativeHistory.scripts.${key} must be a non-empty string`)
|
|
278
284
|
}
|
|
279
285
|
}
|
|
280
286
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Provider SDK
|
|
2
|
+
|
|
3
|
+
> Status: Phase 1 in progress · License of this directory: AGPL-3.0 (part of `oss/`) · See [marketplace plan](../../../../../../docs/design/v1.0.0-marketplace-plan.md).
|
|
4
|
+
|
|
5
|
+
This directory is the **Provider SDK** — the framework that lets external developers author CLI providers (and later IDE, Extension, ACP) against a versioned, typed contract.
|
|
6
|
+
|
|
7
|
+
It lives **inside daemon-core**, not as a separate package, by design:
|
|
8
|
+
|
|
9
|
+
- the contract definitions live next to the runtime that consumes them, eliminating version drift
|
|
10
|
+
- `adhdev provider *` CLI commands call straight into these modules
|
|
11
|
+
- two npm packages are extracted from this source at build time:
|
|
12
|
+
- `@adhdev/provider-types` (Apache 2.0) — TypeScript types for external authors
|
|
13
|
+
- `@adhdev/provider-schemas` (Apache 2.0) — JSON Schemas for editor support
|
|
14
|
+
|
|
15
|
+
## Layout
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
sdk/
|
|
19
|
+
v1/ ← contract version 1 (current)
|
|
20
|
+
types/ ← TypeScript types — extracted to @adhdev/provider-types
|
|
21
|
+
cli/ ← CLI category types
|
|
22
|
+
common/ ← shared types (settings, capabilities, auth, spawn)
|
|
23
|
+
schemas/ ← JSON Schemas — extracted to @adhdev/provider-schemas
|
|
24
|
+
cli/
|
|
25
|
+
provider.schema.json ← top-level provider.json (mirrors adhdev-providers/schemas/v1/cli/)
|
|
26
|
+
primitives/ ← per-primitive schemas, namespaced by category
|
|
27
|
+
common/
|
|
28
|
+
primitives/ ← daemon-side primitive implementations
|
|
29
|
+
cli/ ← TUI primitives, native-history adapters, capability handlers
|
|
30
|
+
builders/ ← functions that turn manifest blocks into runtime handlers
|
|
31
|
+
validators/ ← schema check, AST taint analysis, fixture replay
|
|
32
|
+
scaffolders/ ← `adhdev provider init` templates
|
|
33
|
+
fixture-tooling/ ← PTY capture + replay utilities
|
|
34
|
+
v2/ ← reserved for next contract major
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Primitive identifiers
|
|
38
|
+
|
|
39
|
+
Primitives are referenced by `$schema: adhdev:<category>/<id>@<version>` in provider manifests. See the audit-derived v1 catalog of 50 primitives at [`audit-cli-v1.md §5`](../../../../../../../adhdev-providers/docs/provider-contract/cli/audit-cli-v1.md#5-proposed-v1-primitive-set).
|
|
40
|
+
|
|
41
|
+
## Stability
|
|
42
|
+
|
|
43
|
+
`v1/` is unstable until SDK `1.0.0` is published. Breaking changes between SDK `0.x` releases are permitted during Phase 1-2 migration. After SDK `1.0.0`, breaking changes require an `engines.adhdev` major bump.
|
|
44
|
+
|
|
45
|
+
## Out of scope
|
|
46
|
+
|
|
47
|
+
- This SDK does not provide IDE, Extension, or ACP primitives in v1.0.0. They follow in v1.1+.
|
|
48
|
+
- This SDK does not include the registry server code — that lives in [`oss/packages/registry/`](../../../../../registry/) (not yet created).
|
|
49
|
+
- This SDK does not include the marketplace web UI — that lives separately.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildDetectStatusFromAcp
|
|
3
|
+
*
|
|
4
|
+
* Turns a declarative `acp/session-protocol@1` block into a runtime
|
|
5
|
+
* `(input: AcpStatusInput) => 'idle' | 'generating' | 'waiting_approval' | null`
|
|
6
|
+
* function.
|
|
7
|
+
*
|
|
8
|
+
* Dispatch order:
|
|
9
|
+
* 1. Not connected → null (no information available).
|
|
10
|
+
* 2. Error patterns — checked first; first match returns the declared verdict.
|
|
11
|
+
* 3. Generating pattern — `generating`.
|
|
12
|
+
* 4. Idle pattern — `idle`.
|
|
13
|
+
* 5. Default: `null` (no change — caller preserves last known status).
|
|
14
|
+
*
|
|
15
|
+
* All regex patterns are compiled once at builder time so the returned
|
|
16
|
+
* detector function is allocation-free on the hot path.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ─── Spec shapes (mirror the JSON schema) ──────────────────────────────
|
|
20
|
+
|
|
21
|
+
interface AcpRegexPatternSpec {
|
|
22
|
+
regex: string;
|
|
23
|
+
flags?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface AcpErrorPatternSpec extends AcpRegexPatternSpec {
|
|
28
|
+
verdict: 'idle' | 'generating' | 'waiting_approval';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AcpSessionSpec {
|
|
32
|
+
$schema?: 'adhdev:acp/session-protocol@1';
|
|
33
|
+
/** Pattern that matches a line emitted when the agent is idle/ready. */
|
|
34
|
+
idlePattern?: AcpRegexPatternSpec;
|
|
35
|
+
/** Pattern that matches a line emitted while the agent is actively working. */
|
|
36
|
+
generatingPattern?: AcpRegexPatternSpec;
|
|
37
|
+
/**
|
|
38
|
+
* Error/edge-case patterns. Checked in order; first match returns the
|
|
39
|
+
* declared verdict. Useful for approval prompts, fatal errors, etc.
|
|
40
|
+
*/
|
|
41
|
+
errorPatterns?: AcpErrorPatternSpec[];
|
|
42
|
+
/** Wire format; informational only at the builder level — regex matching
|
|
43
|
+
* is identical regardless of promptStyle. */
|
|
44
|
+
promptStyle?: 'json-rpc' | 'plain-text' | 'mcp';
|
|
45
|
+
/** Message delimiter used to split incoming bytes (default: newline). */
|
|
46
|
+
messageDelimiter?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Input shape ────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export interface AcpStatusInput {
|
|
52
|
+
/** The most recently received line from the stdio stream. */
|
|
53
|
+
lastLine: string;
|
|
54
|
+
/**
|
|
55
|
+
* A sliding window of recent output lines (newest last).
|
|
56
|
+
* Builders may check these when a single line is not enough for context.
|
|
57
|
+
*/
|
|
58
|
+
recentLines: string[];
|
|
59
|
+
/** Whether the ACP process is currently connected and running. */
|
|
60
|
+
isConnected: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Output type ────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export type AcpDetectedStatus = 'idle' | 'generating' | 'waiting_approval' | null;
|
|
66
|
+
|
|
67
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function compile(re: string, flags?: string): RegExp {
|
|
70
|
+
try {
|
|
71
|
+
return new RegExp(re, flags ?? 'i');
|
|
72
|
+
} catch (e) {
|
|
73
|
+
throw new Error(`Invalid regex /${re}/${flags ?? 'i'}: ${(e as Error).message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Safe `.test()` wrapper: resets `lastIndex` before each call so that
|
|
79
|
+
* regexes compiled with the `g` or `y` flag remain stateless across
|
|
80
|
+
* multiple detect invocations.
|
|
81
|
+
*/
|
|
82
|
+
function testRe(re: RegExp, text: string): boolean {
|
|
83
|
+
re.lastIndex = 0;
|
|
84
|
+
return re.test(text);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Compiled internal representation ───────────────────────────────────
|
|
88
|
+
|
|
89
|
+
interface CompiledAcpSpec {
|
|
90
|
+
idle: RegExp | null;
|
|
91
|
+
generating: RegExp | null;
|
|
92
|
+
errors: Array<{ re: RegExp; verdict: AcpDetectedStatus }>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function compileSpec(spec: AcpSessionSpec): CompiledAcpSpec {
|
|
96
|
+
const idle = spec.idlePattern
|
|
97
|
+
? compile(spec.idlePattern.regex, spec.idlePattern.flags)
|
|
98
|
+
: null;
|
|
99
|
+
|
|
100
|
+
const generating = spec.generatingPattern
|
|
101
|
+
? compile(spec.generatingPattern.regex, spec.generatingPattern.flags)
|
|
102
|
+
: null;
|
|
103
|
+
|
|
104
|
+
const errors = (spec.errorPatterns ?? []).map((ep) => ({
|
|
105
|
+
re: compile(ep.regex, ep.flags),
|
|
106
|
+
verdict: ep.verdict as AcpDetectedStatus,
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
return { idle, generating, errors };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Public builder ─────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
export function buildDetectStatusFromAcp(
|
|
115
|
+
spec: AcpSessionSpec,
|
|
116
|
+
): (input: AcpStatusInput) => AcpDetectedStatus {
|
|
117
|
+
const compiled = compileSpec(spec);
|
|
118
|
+
|
|
119
|
+
return function detectAcpStatus(input: AcpStatusInput): AcpDetectedStatus {
|
|
120
|
+
// 1. No connection → no information.
|
|
121
|
+
if (!input.isConnected) return null;
|
|
122
|
+
|
|
123
|
+
const line = input.lastLine;
|
|
124
|
+
|
|
125
|
+
// 2. Error/edge-case patterns take priority.
|
|
126
|
+
for (const { re, verdict } of compiled.errors) {
|
|
127
|
+
if (testRe(re, line)) return verdict;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 3. Generating pattern.
|
|
131
|
+
if (compiled.generating && testRe(compiled.generating, line)) return 'generating';
|
|
132
|
+
|
|
133
|
+
// 4. Idle pattern.
|
|
134
|
+
if (compiled.idle && testRe(compiled.idle, line)) return 'idle';
|
|
135
|
+
|
|
136
|
+
// 5. Nothing matched.
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Internal exports for builder reuse + tests.
|
|
142
|
+
export const __internal = {
|
|
143
|
+
compileSpec,
|
|
144
|
+
};
|