@adhdev/daemon-core 0.9.82-rc.142 → 0.9.82-rc.144

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 (91) hide show
  1. package/dist/boot/process-hardening.d.ts +50 -0
  2. package/dist/cli-adapters/cli-script-runner.d.ts +73 -1
  3. package/dist/cli-adapters/provider-cli-adapter.d.ts +17 -0
  4. package/dist/cli-adapters/provider-cli-shared.d.ts +6 -0
  5. package/dist/commands/handler.d.ts +66 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +2876 -403
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2890 -424
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/ipc/local-ipc-server.d.ts +91 -0
  12. package/dist/providers/contracts.d.ts +8 -0
  13. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  14. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  15. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  16. package/dist/providers/native-history/index.d.ts +11 -0
  17. package/dist/providers/provider-loader.d.ts +19 -1
  18. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  19. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  20. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  21. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +64 -0
  22. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +82 -0
  23. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  24. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  25. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  26. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  27. package/dist/providers/sdk/v1/index.d.ts +30 -0
  28. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  29. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  30. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  31. package/dist/providers/sdk/v1/types/cli/index.d.ts +268 -0
  32. package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
  33. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  34. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  35. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  36. package/package.json +4 -2
  37. package/src/boot/daemon-lifecycle.ts +14 -10
  38. package/src/boot/process-hardening.ts +89 -0
  39. package/src/cli-adapters/cli-script-runner.ts +289 -13
  40. package/src/cli-adapters/cli-state-engine.ts +8 -5
  41. package/src/cli-adapters/provider-cli-adapter.ts +36 -2
  42. package/src/cli-adapters/provider-cli-shared.ts +6 -0
  43. package/src/commands/chat-commands.ts +22 -1
  44. package/src/commands/cli-manager.ts +39 -0
  45. package/src/commands/handler.ts +539 -1
  46. package/src/commands/router.ts +1 -0
  47. package/src/index.ts +27 -0
  48. package/src/ipc/local-ipc-server.ts +278 -0
  49. package/src/providers/cli-provider-instance.ts +15 -0
  50. package/src/providers/contracts.ts +8 -0
  51. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  52. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  53. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  54. package/src/providers/native-history/index.ts +23 -0
  55. package/src/providers/provider-loader.ts +258 -17
  56. package/src/providers/provider-schema.ts +3 -0
  57. package/src/providers/sdk/README.md +49 -0
  58. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  59. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  60. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  61. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +245 -0
  62. package/src/providers/sdk/v1/builders/cli/parse-session.ts +247 -0
  63. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  64. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  65. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  66. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  67. package/src/providers/sdk/v1/index.ts +151 -0
  68. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  69. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  70. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  71. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +428 -0
  72. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  73. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  74. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  75. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  76. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  77. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  78. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  79. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  80. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +119 -0
  81. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  82. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  83. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  84. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  85. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  86. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  87. package/src/providers/sdk/v1/types/cli/index.ts +355 -0
  88. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  89. package/src/providers/sdk/v1/validators/index.ts +19 -0
  90. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  91. 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
- return [this.userDir, this.upstreamDir];
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 user custom (excluding .upstream — highest priority, never auto-updated)
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
- if (base.overrides) {
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.js or provider.json) */
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
- for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
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 (fs.existsSync(path.join(candidate, 'provider.json'))) return candidate;
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 jsonPath = path.join(catDir, entry.name, 'provider.json');
1615
- if (fs.existsSync(jsonPath)) {
1616
- try {
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
- // Check if this directory has provider.json
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 jsonPath = path.join(d, 'provider.json');
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,17 @@ export class ProviderLoader {
1732
1958
  ...(extensionIdPattern instanceof RegExp ? { extensionIdPattern } : {}),
1733
1959
  };
1734
1960
 
1961
+ // v1 manifest contract calls this block `nativeHistory`; the
1962
+ // daemon's runtime + downstream code still reads
1963
+ // `canonicalHistory` (the legacy name used since v0). Alias
1964
+ // them when only the v1 spelling is present so authors using
1965
+ // the new contract get the same behavior without having to
1966
+ // duplicate the block. If both are set the explicit
1967
+ // canonicalHistory wins — caller signaled it intentionally.
1968
+ if ((normalizedProvider as any).nativeHistory && !(normalizedProvider as any).canonicalHistory) {
1969
+ (normalizedProvider as any).canonicalHistory = (normalizedProvider as any).nativeHistory;
1970
+ }
1971
+
1735
1972
  const validation = validateProviderDefinition(normalizedProvider);
1736
1973
  for (const warning of validation.warnings) {
1737
1974
  this.log(`⚠ ${jsonPath}: ${warning}`);
@@ -1745,6 +1982,10 @@ export class ProviderLoader {
1745
1982
  const scriptsPath = path.join(d, 'scripts.js');
1746
1983
  if (!hasCompatibility && fs.existsSync(scriptsPath)) {
1747
1984
  try {
1985
+ // Gate the IDE/extension scripts.js (legacy single-file
1986
+ // format) under the same whitelist. `d` here is the
1987
+ // provider dir; its grandparent contains _shared.
1988
+ registerProviderScriptRootSafely(path.dirname(path.dirname(d)));
1748
1989
  delete require.cache[require.resolve(scriptsPath)];
1749
1990
  const scripts = require(scriptsPath) as Partial<ProviderScripts>;
1750
1991
  normalizedProvider.scripts = scripts;
@@ -38,6 +38,9 @@ const KNOWN_PROVIDER_FIELDS = new Set<string>([
38
38
  'sessionIdPattern',
39
39
  'historyBehavior',
40
40
  'canonicalHistory',
41
+ // v1 contract spelling; the loader aliases nativeHistory →
42
+ // canonicalHistory so downstream code reads the legacy name.
43
+ 'nativeHistory',
41
44
  'autoFixProfile',
42
45
  'ideLevelScripts',
43
46
  'allowInputDuringGeneration',
@@ -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
+ };