@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.
Files changed (92) 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 +2943 -435
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +2957 -456
  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 +31 -5
  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 +91 -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 +277 -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 +35 -14
  44. package/src/commands/cli-manager.ts +39 -0
  45. package/src/commands/handler.ts +539 -1
  46. package/src/commands/router.ts +2 -1
  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 +18 -3
  50. package/src/providers/contracts.ts +33 -5
  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 +263 -17
  56. package/src/providers/provider-schema.ts +21 -15
  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 +276 -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 +152 -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 +444 -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-session-id-extraction-v1.json +46 -0
  83. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  84. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  85. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  86. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  87. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  88. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  89. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  90. package/src/providers/sdk/v1/validators/index.ts +19 -0
  91. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  92. 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,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
- validateCanonicalHistory(provider.canonicalHistory, errors)
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 validateCanonicalHistory(raw: unknown, errors: string[]): void {
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('canonicalHistory must be an object')
242
+ errors.push('nativeHistory must be an object')
237
243
  return
238
244
  }
239
245
 
240
- const canonicalHistory = raw as Record<string, unknown>
241
- const format = canonicalHistory.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('canonicalHistory.format must be a non-empty string when provided')
249
+ errors.push('nativeHistory.format must be a non-empty string when provided')
244
250
  }
245
251
 
246
- const watchPath = canonicalHistory.watchPath
252
+ const watchPath = nativeHistory.watchPath
247
253
  if (watchPath !== undefined && (typeof watchPath !== 'string' || !watchPath.trim())) {
248
- errors.push('canonicalHistory.watchPath must be a non-empty string when provided')
254
+ errors.push('nativeHistory.watchPath must be a non-empty string when provided')
249
255
  }
250
256
 
251
- const mode = canonicalHistory.mode
257
+ const mode = nativeHistory.mode
252
258
  if (mode !== undefined && !['native-source', 'materialized-mirror', 'disabled'].includes(String(mode))) {
253
- errors.push('canonicalHistory.mode must be one of: native-source, materialized-mirror, disabled')
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 = canonicalHistory.contractVersion
265
+ const chatContractVersion = nativeHistory.contractVersion
260
266
  if (chatContractVersion !== undefined
261
267
  && chatContractVersion !== '1.0'
262
268
  && chatContractVersion !== '2.0') {
263
- errors.push(`canonicalHistory.contractVersion must be '1.0' or '2.0' when provided (got ${JSON.stringify(chatContractVersion)})`)
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 = canonicalHistory.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('canonicalHistory.scripts must be an object')
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(`canonicalHistory.scripts.${key} must be a non-empty string`)
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
+ };