@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
@@ -1154,6 +1154,45 @@ export class DaemonCliManager {
1154
1154
  this.deps.onStatusChange();
1155
1155
  return { success: true, id: found.key, mode };
1156
1156
  }
1157
+ case 'record_provider_pty': {
1158
+ const cliType = args?.type || args?.cliType;
1159
+ if (!cliType) {
1160
+ return { success: false, error: '`type` (provider type) is required', code: 'MISSING_TYPE' };
1161
+ }
1162
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId : '';
1163
+ const dir = args?.dir || '';
1164
+ const found = (targetSessionId ? this.findAdapterBySessionId(targetSessionId) : null)
1165
+ || this.findAdapter(cliType, { instanceKey: targetSessionId, dir });
1166
+ if (!found) {
1167
+ return {
1168
+ success: false,
1169
+ error: `No running ${cliType} session. Launch one first (adhdev launch ${cliType}) or pass --target-session-id.`,
1170
+ code: 'NO_RUNNING_SESSION',
1171
+ };
1172
+ }
1173
+ const instance = this.deps.getInstanceManager()?.getInstance(found.key);
1174
+ if (!(instance instanceof CliProviderInstance)) {
1175
+ return { success: false, error: 'CLI instance not available', code: 'CLI_INSTANCE_NOT_FOUND' };
1176
+ }
1177
+ const adapter = instance.getAdapter();
1178
+ if (!adapter || typeof (adapter as any).getAccumulatedRawBuffer !== 'function') {
1179
+ return { success: false, error: 'Adapter does not expose PTY buffer', code: 'ADAPTER_NOT_RECORDABLE' };
1180
+ }
1181
+ const buffer = (adapter as any).getAccumulatedRawBuffer() as { text: string; droppedChars: number };
1182
+ const maxBytes = Number(args?.maxBytes) > 0 ? Number(args.maxBytes) : 262144;
1183
+ const truncated = buffer.text.length > maxBytes;
1184
+ const ptyBytes = truncated ? buffer.text.slice(-maxBytes) : buffer.text;
1185
+ return {
1186
+ success: true,
1187
+ cliType,
1188
+ sessionId: found.key,
1189
+ ptyBytes,
1190
+ bytes: ptyBytes.length,
1191
+ truncated,
1192
+ droppedChars: buffer.droppedChars,
1193
+ capturedAt: Date.now(),
1194
+ };
1195
+ }
1157
1196
  case 'restart_session': {
1158
1197
  const cliType = args?.cliType || args?.agentType || args?.ideType;
1159
1198
  const cfg = loadConfig();
@@ -510,6 +510,11 @@ export class DaemonCommandHandler implements CommandHelpers {
510
510
 
511
511
  // ─── Script manage ───────────────────
512
512
  case 'refresh_scripts': return this.handleRefreshScripts(args);
513
+ case 'list_provider_availability': return this.handleListProviderAvailability(args);
514
+ case 'install_provider_manifest': return this.handleInstallProviderManifest(args);
515
+ case 'uninstall_provider_manifest': return this.handleUninstallProviderManifest(args);
516
+ case 'check_provider_updates': return this.handleCheckProviderUpdates(args);
517
+ case 'list_installed_providers': return this.handleListInstalledProviders(args);
513
518
 
514
519
  // ─── Stream commands (stream-commands.ts) ───────────
515
520
  case 'select_session': return Stream.handleSelectSession(this, args);
@@ -545,9 +550,15 @@ export class DaemonCommandHandler implements CommandHelpers {
545
550
 
546
551
  // ─── Misc (kept in handler — too small to extract) ───────
547
552
 
553
+ /**
554
+ * Reload providers from disk. Does NOT pull from the registry — the user
555
+ * controls installs explicitly via install_provider_manifest. To upgrade
556
+ * an installed provider, call install_provider_manifest again with the
557
+ * desired version (or with no version to pick up the latest from
558
+ * registry), or use check_provider_updates to see what is out of date.
559
+ */
548
560
  private async handleRefreshScripts(_args: any): Promise<CommandResult> {
549
561
  if (this._ctx.providerLoader) {
550
- await this._ctx.providerLoader.fetchLatest().catch(() => {});
551
562
  this._ctx.providerLoader.reload();
552
563
  this._ctx.providerLoader.registerToDetector();
553
564
  const refreshedInstances = this._ctx.instanceManager
@@ -563,6 +574,533 @@ export class DaemonCommandHandler implements CommandHelpers {
563
574
  return { success: false, error: 'ProviderLoader not initialized' };
564
575
  }
565
576
 
577
+ /**
578
+ * Return per-provider availability so a Marketplace UI can show
579
+ * "Installed" badges. Reuses the existing detection state from
580
+ * ProviderLoader.getMachineProviderStatus() — no probing is triggered.
581
+ */
582
+ private handleListProviderAvailability(_args: any): CommandResult {
583
+ if (!this._ctx.providerLoader) {
584
+ return { success: false, error: 'ProviderLoader not initialized' };
585
+ }
586
+ const loader = this._ctx.providerLoader;
587
+ const items = loader.getAll().map((provider) => {
588
+ const machineConfig = loader.getMachineProviderConfig(provider.type);
589
+ const lastDetection = machineConfig.lastDetection;
590
+ return {
591
+ type: provider.type,
592
+ category: provider.category,
593
+ status: loader.getMachineProviderStatus(provider.type),
594
+ installed: lastDetection?.ok === true,
595
+ detectedPath: lastDetection?.path ?? null,
596
+ checkedAt: lastDetection?.checkedAt ?? null,
597
+ };
598
+ });
599
+ return { success: true, providers: items };
600
+ }
601
+
602
+ /**
603
+ * Compute the *Marketplace install root*. This is always
604
+ * `~/.adhdev/marketplace/` regardless of how ProviderLoader resolved its
605
+ * userDir (which can point to a sibling adhdev-providers git checkout in
606
+ * dev). Marketplace-installed manifests must never overwrite files in a
607
+ * developer checkout, and they should be isolated from upstream/registry
608
+ * sync so they survive `refresh_scripts`.
609
+ */
610
+ private getMarketplaceInstallRoot(): string {
611
+ const os = require('os') as typeof import('os');
612
+ const path = require('path') as typeof import('path');
613
+ return path.join(os.homedir(), '.adhdev', 'marketplace');
614
+ }
615
+
616
+ /**
617
+ * Download a single provider manifest from the registry and write it to
618
+ * ~/.adhdev/marketplace/{category}/{type}/provider.json.
619
+ *
620
+ * Used by the Marketplace UI's Install button. Verifies SHA-256 checksum
621
+ * against the registry meta before persisting. Refuses to write outside
622
+ * the marketplace root.
623
+ *
624
+ * Args: { type: string, category?: string, version?: string }
625
+ * If category/version are omitted, looks up the latest from the registry.
626
+ */
627
+ private async handleInstallProviderManifest(args: any): Promise<CommandResult> {
628
+ if (!this._ctx.providerLoader) {
629
+ return { success: false, error: 'ProviderLoader not initialized' };
630
+ }
631
+ const type = typeof args?.type === 'string' ? args.type : '';
632
+ if (!type) return { success: false, error: 'type is required' };
633
+ // Defense in depth: reject any obvious path-traversal in the type.
634
+ if (!/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(type)) {
635
+ return { success: false, error: 'invalid type' };
636
+ }
637
+
638
+ const https = require('https') as typeof import('https');
639
+ const fs = require('fs') as typeof import('fs');
640
+ const path = require('path') as typeof import('path');
641
+ const crypto = require('crypto') as typeof import('crypto');
642
+ const REGISTRY = 'https://api.adhf.dev/api/v1/registry';
643
+
644
+ function fetchText(url: string, timeoutMs: number): Promise<string> {
645
+ return new Promise((resolve, reject) => {
646
+ const req = https.get(url, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: timeoutMs }, (res) => {
647
+ if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
648
+ const chunks: Buffer[] = [];
649
+ res.on('data', (c: Buffer) => chunks.push(c));
650
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
651
+ });
652
+ req.on('error', reject);
653
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
654
+ });
655
+ }
656
+
657
+ try {
658
+ // 1. Look up provider metadata so we know the expected category, version, checksum.
659
+ const metaBody = await fetchText(`${REGISTRY}/providers/${encodeURIComponent(type)}`, 10000);
660
+ const meta = JSON.parse(metaBody) as { type: string; category: string; version: string; checksum: string };
661
+ const category = typeof args?.category === 'string' ? args.category : meta.category;
662
+ const version = typeof args?.version === 'string' ? args.version : meta.version;
663
+
664
+ // Defense in depth on category as well — only known categories.
665
+ if (!['cli', 'ide', 'extension', 'acp'].includes(category)) {
666
+ return { success: false, error: `unknown category: ${category}` };
667
+ }
668
+
669
+ // 2. Download the manifest body.
670
+ const manifestBody = await fetchText(
671
+ `${REGISTRY}/providers/${encodeURIComponent(type)}/${encodeURIComponent(version)}/download`,
672
+ 30000
673
+ );
674
+
675
+ // 3. Verify checksum.
676
+ const actualChecksum = crypto.createHash('sha256').update(manifestBody, 'utf-8').digest('hex');
677
+ if (actualChecksum !== meta.checksum) {
678
+ return { success: false, error: `checksum mismatch: expected ${meta.checksum}, got ${actualChecksum}` };
679
+ }
680
+
681
+ // 4. Write to the marketplace install root, NOT to ProviderLoader.getUserDir():
682
+ // in dev, userDir points at the sibling adhdev-providers git checkout.
683
+ const installRoot = this.getMarketplaceInstallRoot();
684
+ const installRootResolved = path.resolve(installRoot);
685
+ const targetDir = path.resolve(path.join(installRoot, category, type));
686
+ if (!targetDir.startsWith(installRootResolved + path.sep)) {
687
+ return { success: false, error: 'install path escaped marketplace root' };
688
+ }
689
+ fs.mkdirSync(targetDir, { recursive: true });
690
+ // v1 vs v0 manifest selection — v1 manifests carry an SDK
691
+ // $schema URL or v1-only keys (tui, overrides-as-object,
692
+ // source, canonicalHistory). The loader prefers
693
+ // provider.v1.json when both exist, so writing v1 manifests
694
+ // under the v0 name would shadow them. Detect and write to
695
+ // the right file.
696
+ let manifestProbe: Record<string, any> = {};
697
+ try { manifestProbe = JSON.parse(manifestBody) as Record<string, any>; } catch { /* validation below */ }
698
+ const isV1 = typeof manifestProbe?.$schema === 'string' && manifestProbe.$schema.includes('/v1/')
699
+ || (manifestProbe?.overrides && typeof manifestProbe.overrides === 'object' && !Array.isArray(manifestProbe.overrides))
700
+ || !!manifestProbe?.tui;
701
+
702
+ // Reject v1 manifests that don't match the schema at install
703
+ // time so the daemon never persists a known-bad manifest.
704
+ // The provider-loader keeps a permissive warn-only behavior
705
+ // for manifests already on disk, but the install path is the
706
+ // right place to fail fast.
707
+ if (isV1 && manifestProbe?.category === 'cli') {
708
+ try {
709
+ const { validateCliProviderManifest, formatManifestValidationIssues } =
710
+ require('../providers/sdk/v1/validators/manifest.js') as typeof import('../providers/sdk/v1/validators/manifest.js');
711
+ const validation = validateCliProviderManifest(manifestProbe);
712
+ if (!validation.ok) {
713
+ return {
714
+ success: false,
715
+ error: `manifest failed v1 schema validation:\n${formatManifestValidationIssues(validation.issues)}`,
716
+ validationIssues: validation.issues,
717
+ };
718
+ }
719
+ } catch (e: any) {
720
+ // Validator load failure shouldn't block install — log
721
+ // and continue. The loader's warn-only path will
722
+ // surface the same issue at boot if it's real.
723
+ LOG.warn('Command', `[install_provider_manifest] schema validator unavailable: ${e?.message || e}`);
724
+ }
725
+ }
726
+
727
+ const targetFile = isV1 ? 'provider.v1.json' : 'provider.json';
728
+ const targetPath = path.join(targetDir, targetFile);
729
+ fs.writeFileSync(targetPath, manifestBody, 'utf-8');
730
+
731
+ // 5. If the manifest declares a `source` GitHub repo, fetch the
732
+ // script directories listed in the manifest (defaultScriptDir +
733
+ // each compatibility[].scriptDir). Extended-tier providers
734
+ // bundle their override JS this way — the registry intentionally
735
+ // only stores the manifest JSON, not the script bytes, so a
736
+ // third-party can publish a manifest pointing at their own fork
737
+ // without pushing files into our R2 bucket.
738
+ const manifestJson = JSON.parse(manifestBody) as Record<string, any>;
739
+ const scriptFetch = await this.fetchProviderSources(
740
+ manifestJson,
741
+ category,
742
+ type,
743
+ targetDir,
744
+ );
745
+
746
+ // Hot-reload so the daemon picks up the new manifest.
747
+ this._ctx.providerLoader.reload();
748
+ this._ctx.providerLoader.registerToDetector();
749
+
750
+ return {
751
+ success: true,
752
+ installed: {
753
+ type, category, version, checksum: actualChecksum, path: targetPath,
754
+ scriptsFetched: scriptFetch.fetchedCount,
755
+ scriptSource: scriptFetch.source,
756
+ scriptErrors: scriptFetch.errors,
757
+ },
758
+ };
759
+ } catch (e: any) {
760
+ return { success: false, error: `install failed: ${e?.message || e}` };
761
+ }
762
+ }
763
+
764
+ /**
765
+ * If `manifest.source = { type:'github', repo, ref, subdir? }` is set,
766
+ * walk each script directory the manifest references and download every
767
+ * file from the public GitHub raw endpoint. Returns a small summary so
768
+ * the caller can report what was fetched.
769
+ *
770
+ * Best-effort: failures don't reject the install — the manifest itself is
771
+ * usable for declarative-only providers, and the user still gets a clear
772
+ * error string back if a needed script is missing.
773
+ */
774
+ private async fetchProviderSources(
775
+ manifest: Record<string, any>,
776
+ category: string,
777
+ type: string,
778
+ targetDir: string,
779
+ ): Promise<{ fetchedCount: number; source: string | null; errors: string[] }> {
780
+ const errors: string[] = [];
781
+ const source = manifest?.source;
782
+ if (!source || source.type !== 'github' || typeof source.repo !== 'string' || typeof source.ref !== 'string') {
783
+ return { fetchedCount: 0, source: null, errors };
784
+ }
785
+
786
+ // Collect every script directory the manifest references. v1 manifests
787
+ // use `defaultScriptDir` and `compatibility[].scriptDir`. We also pull
788
+ // any override path's directory (e.g. overrides.detectStatus.path =
789
+ // "scripts/v1/detect_status.js" → fetch the scripts/v1/ directory too).
790
+ const scriptDirs = new Set<string>();
791
+ if (typeof manifest.defaultScriptDir === 'string') scriptDirs.add(manifest.defaultScriptDir);
792
+ if (Array.isArray(manifest.compatibility)) {
793
+ for (const c of manifest.compatibility) {
794
+ if (typeof c?.scriptDir === 'string') scriptDirs.add(c.scriptDir);
795
+ }
796
+ }
797
+ if (manifest.overrides && typeof manifest.overrides === 'object' && !Array.isArray(manifest.overrides)) {
798
+ for (const override of Object.values(manifest.overrides) as Array<Record<string, unknown>>) {
799
+ const overridePath = override?.path;
800
+ if (typeof overridePath === 'string' && overridePath.includes('/')) {
801
+ const dir = overridePath.substring(0, overridePath.lastIndexOf('/'));
802
+ if (dir) scriptDirs.add(dir);
803
+ }
804
+ }
805
+ }
806
+ if (scriptDirs.size === 0) {
807
+ return { fetchedCount: 0, source: `${source.repo}@${source.ref}`, errors };
808
+ }
809
+
810
+ const subdir: string = typeof source.subdir === 'string' && source.subdir.length > 0
811
+ ? source.subdir
812
+ : `${category}/${type}`;
813
+ const repo: string = source.repo;
814
+ const ref: string = source.ref;
815
+
816
+ const https = require('https') as typeof import('https');
817
+ const fs = require('fs') as typeof import('fs');
818
+ const path = require('path') as typeof import('path');
819
+
820
+ function fetchJson(url: string, timeoutMs: number): Promise<any> {
821
+ return new Promise((resolve, reject) => {
822
+ const req = https.get(url, {
823
+ headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/vnd.github+json' },
824
+ timeout: timeoutMs,
825
+ }, (res) => {
826
+ if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
827
+ const chunks: Buffer[] = [];
828
+ res.on('data', (c: Buffer) => chunks.push(c));
829
+ res.on('end', () => {
830
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8'))); }
831
+ catch (e) { reject(e); }
832
+ });
833
+ });
834
+ req.on('error', reject);
835
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
836
+ });
837
+ }
838
+
839
+ function fetchBinary(url: string, timeoutMs: number): Promise<Buffer> {
840
+ return new Promise((resolve, reject) => {
841
+ const req = https.get(url, {
842
+ headers: { 'User-Agent': 'adhdev-daemon' },
843
+ timeout: timeoutMs,
844
+ }, (res) => {
845
+ // Raw endpoint redirects through codeload — follow the redirect.
846
+ if (res.statusCode === 301 || res.statusCode === 302) {
847
+ if (res.headers.location) {
848
+ return fetchBinary(res.headers.location, timeoutMs).then(resolve, reject);
849
+ }
850
+ }
851
+ if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
852
+ const chunks: Buffer[] = [];
853
+ res.on('data', (c: Buffer) => chunks.push(c));
854
+ res.on('end', () => resolve(Buffer.concat(chunks)));
855
+ });
856
+ req.on('error', reject);
857
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
858
+ });
859
+ }
860
+
861
+ let fetchedCount = 0;
862
+
863
+ // The shared helpers directory (cli/_shared/) is referenced by most
864
+ // CLI providers via `require('../../../_shared/...')`. Fetch it once
865
+ // alongside the provider's own scripts so node's require resolution
866
+ // succeeds at runtime. Best-effort — silent if the source repo has no
867
+ // _shared dir (e.g. ACP-only repos).
868
+ const sharedDirRel = `${category}/_shared`;
869
+ const sharedTargetDir = path.resolve(path.join(targetDir, '../_shared'));
870
+ const installRootResolved = path.resolve(path.join(targetDir, '../..'));
871
+ if (sharedTargetDir.startsWith(installRootResolved + path.sep)) {
872
+ const sharedStack: string[] = [sharedDirRel];
873
+ while (sharedStack.length) {
874
+ const relDir = sharedStack.pop()!;
875
+ const apiUrl = `https://api.github.com/repos/${repo}/contents/${encodeURI(relDir)}?ref=${encodeURIComponent(ref)}`;
876
+ let entries: Array<{ type: string; path: string; name: string; download_url: string | null }>;
877
+ try {
878
+ entries = await fetchJson(apiUrl, 15000);
879
+ } catch (e: any) {
880
+ // Silent: _shared may not exist on third-party repos
881
+ if (relDir === sharedDirRel) break;
882
+ errors.push(`list shared ${relDir}: ${e?.message ?? e}`);
883
+ continue;
884
+ }
885
+ if (!Array.isArray(entries)) continue;
886
+ for (const entry of entries) {
887
+ if (entry.type === 'dir') { sharedStack.push(entry.path); continue; }
888
+ if (entry.type !== 'file' || !entry.download_url) continue;
889
+ try {
890
+ const body = await fetchBinary(entry.download_url, 30000);
891
+ // entry.path is like 'cli/_shared/foo.js' — strip 'cli/_shared/' prefix
892
+ const relInside = entry.path.startsWith(sharedDirRel + '/')
893
+ ? entry.path.slice(sharedDirRel.length + 1)
894
+ : entry.path;
895
+ const outPath = path.resolve(path.join(sharedTargetDir, relInside));
896
+ if (!outPath.startsWith(path.resolve(sharedTargetDir) + path.sep)) continue;
897
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
898
+ fs.writeFileSync(outPath, body);
899
+ fetchedCount++;
900
+ } catch (e: any) {
901
+ errors.push(`fetch shared ${entry.path}: ${e?.message ?? e}`);
902
+ }
903
+ }
904
+ }
905
+ }
906
+
907
+ for (const scriptDir of scriptDirs) {
908
+ // GitHub Contents API returns the file list under the dir. We
909
+ // recurse into subdirectories so e.g. scripts/v1/helpers/foo.js is
910
+ // captured too.
911
+ const stack: string[] = [`${subdir}/${scriptDir}`];
912
+ while (stack.length) {
913
+ const relDir = stack.pop()!;
914
+ const apiUrl = `https://api.github.com/repos/${repo}/contents/${encodeURI(relDir)}?ref=${encodeURIComponent(ref)}`;
915
+ let entries: Array<{ type: string; path: string; name: string; download_url: string | null }>;
916
+ try {
917
+ entries = await fetchJson(apiUrl, 15000);
918
+ } catch (e: any) {
919
+ errors.push(`list ${relDir}: ${e?.message ?? e}`);
920
+ continue;
921
+ }
922
+ if (!Array.isArray(entries)) {
923
+ errors.push(`list ${relDir}: unexpected response shape`);
924
+ continue;
925
+ }
926
+ for (const entry of entries) {
927
+ if (entry.type === 'dir') {
928
+ stack.push(entry.path);
929
+ continue;
930
+ }
931
+ if (entry.type !== 'file' || !entry.download_url) continue;
932
+ try {
933
+ const body = await fetchBinary(entry.download_url, 30000);
934
+ // entry.path is relative to repo root → strip the repo subdir prefix
935
+ // so the path inside targetDir matches the layout the loader expects.
936
+ const relInsideProvider = entry.path.startsWith(subdir + '/')
937
+ ? entry.path.slice(subdir.length + 1)
938
+ : entry.path;
939
+ const outPath = path.resolve(path.join(targetDir, relInsideProvider));
940
+ // Path-traversal guard.
941
+ if (!outPath.startsWith(path.resolve(targetDir) + path.sep)) {
942
+ errors.push(`refusing to write outside targetDir: ${entry.path}`);
943
+ continue;
944
+ }
945
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
946
+ fs.writeFileSync(outPath, body);
947
+ fetchedCount++;
948
+ } catch (e: any) {
949
+ errors.push(`fetch ${entry.path}: ${e?.message ?? e}`);
950
+ }
951
+ }
952
+ }
953
+ }
954
+
955
+ return { fetchedCount, source: `${repo}@${ref}`, errors };
956
+ }
957
+
958
+ /**
959
+ * Remove a provider manifest from the marketplace install root
960
+ * (~/.adhdev/marketplace/{category}/{type}/). Refuses to touch anything
961
+ * outside that root.
962
+ */
963
+ private async handleUninstallProviderManifest(args: any): Promise<CommandResult> {
964
+ const type = typeof args?.type === 'string' ? args.type : '';
965
+ const category = typeof args?.category === 'string' ? args.category : '';
966
+ if (!type || !category) return { success: false, error: 'type and category are required' };
967
+ if (!/^[a-z0-9][a-z0-9_-]{0,63}$/i.test(type)) {
968
+ return { success: false, error: 'invalid type' };
969
+ }
970
+ if (!['cli', 'ide', 'extension', 'acp'].includes(category)) {
971
+ return { success: false, error: `unknown category: ${category}` };
972
+ }
973
+
974
+ const fs = require('fs') as typeof import('fs');
975
+ const path = require('path') as typeof import('path');
976
+
977
+ try {
978
+ const installRoot = this.getMarketplaceInstallRoot();
979
+ const installRootResolved = path.resolve(installRoot);
980
+ const targetDir = path.resolve(path.join(installRoot, category, type));
981
+
982
+ if (!targetDir.startsWith(installRootResolved + path.sep)) {
983
+ return { success: false, error: 'refusing to delete outside marketplace root' };
984
+ }
985
+ if (!fs.existsSync(targetDir)) {
986
+ return { success: false, error: 'not installed' };
987
+ }
988
+
989
+ fs.rmSync(targetDir, { recursive: true, force: true });
990
+
991
+ if (this._ctx.providerLoader) {
992
+ this._ctx.providerLoader.reload();
993
+ this._ctx.providerLoader.registerToDetector();
994
+ }
995
+
996
+ return { success: true, removed: { type, category, path: targetDir } };
997
+ } catch (e: any) {
998
+ return { success: false, error: `uninstall failed: ${e?.message || e}` };
999
+ }
1000
+ }
1001
+
1002
+ /**
1003
+ * Return everything currently installed in ~/.adhdev/marketplace/ with its
1004
+ * version. This is the "what does this daemon have" answer used both by
1005
+ * the UI and by the update checker.
1006
+ */
1007
+ private handleListInstalledProviders(_args: any): CommandResult {
1008
+ const fs = require('fs') as typeof import('fs');
1009
+ const path = require('path') as typeof import('path');
1010
+
1011
+ const installRoot = this.getMarketplaceInstallRoot();
1012
+ if (!fs.existsSync(installRoot)) return { success: true, providers: [] };
1013
+
1014
+ const CATEGORIES = ['cli', 'ide', 'extension', 'acp'] as const;
1015
+ const items: Array<{ type: string; category: string; version: string; path: string }> = [];
1016
+
1017
+ for (const category of CATEGORIES) {
1018
+ const categoryDir = path.join(installRoot, category);
1019
+ if (!fs.existsSync(categoryDir)) continue;
1020
+ let entries: string[];
1021
+ try { entries = fs.readdirSync(categoryDir); } catch { continue; }
1022
+ for (const type of entries) {
1023
+ // v1 manifest takes precedence over v0 when both are present.
1024
+ const v1Path = path.join(categoryDir, type, 'provider.v1.json');
1025
+ const v0Path = path.join(categoryDir, type, 'provider.json');
1026
+ const manifestPath = fs.existsSync(v1Path) ? v1Path : (fs.existsSync(v0Path) ? v0Path : null);
1027
+ if (!manifestPath) continue;
1028
+ try {
1029
+ const m = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
1030
+ items.push({
1031
+ type,
1032
+ category,
1033
+ version: typeof m.providerVersion === 'string' ? m.providerVersion : '0.0.0',
1034
+ path: manifestPath,
1035
+ });
1036
+ } catch {
1037
+ // Corrupt manifest — skip but don't fail the whole listing.
1038
+ }
1039
+ }
1040
+ }
1041
+ return { success: true, providers: items };
1042
+ }
1043
+
1044
+ /**
1045
+ * For each installed provider, ask the registry for its current latest
1046
+ * version and report whether an update is available. The user can then
1047
+ * call install_provider_manifest to upgrade (it overwrites the file).
1048
+ *
1049
+ * Returns { providers: [{ type, category, installedVersion, latestVersion,
1050
+ * updateAvailable, error? }] }
1051
+ */
1052
+ private async handleCheckProviderUpdates(_args: any): Promise<CommandResult> {
1053
+ const installed = this.handleListInstalledProviders({});
1054
+ if (!installed.success) return installed;
1055
+
1056
+ const https = require('https') as typeof import('https');
1057
+ const REGISTRY = 'https://api.adhf.dev/api/v1/registry';
1058
+
1059
+ function fetchJson(url: string): Promise<any> {
1060
+ return new Promise((resolve, reject) => {
1061
+ const req = https.get(url, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: 10000 }, (res) => {
1062
+ if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
1063
+ const chunks: Buffer[] = [];
1064
+ res.on('data', (c: Buffer) => chunks.push(c));
1065
+ res.on('end', () => {
1066
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf-8'))); }
1067
+ catch (e) { reject(e); }
1068
+ });
1069
+ });
1070
+ req.on('error', reject);
1071
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
1072
+ });
1073
+ }
1074
+
1075
+ const installedList = (installed as unknown as { providers: Array<{ type: string; category: string; version: string }> }).providers;
1076
+ const checks = await Promise.all(
1077
+ installedList.map(async (p) => {
1078
+ try {
1079
+ const remote = await fetchJson(`${REGISTRY}/providers/${encodeURIComponent(p.type)}`);
1080
+ const latestVersion = String(remote?.version ?? '');
1081
+ return {
1082
+ type: p.type,
1083
+ category: p.category,
1084
+ installedVersion: p.version,
1085
+ latestVersion,
1086
+ updateAvailable: latestVersion !== '' && latestVersion !== p.version,
1087
+ };
1088
+ } catch (e: any) {
1089
+ return {
1090
+ type: p.type,
1091
+ category: p.category,
1092
+ installedVersion: p.version,
1093
+ latestVersion: null,
1094
+ updateAvailable: false,
1095
+ error: e?.message ?? String(e),
1096
+ };
1097
+ }
1098
+ })
1099
+ );
1100
+
1101
+ return { success: true, providers: checks };
1102
+ }
1103
+
566
1104
  // ─── DevServer HTTP proxy helpers ─────────────────
567
1105
  // These bridge WS commands to the DevServer REST API (localhost:19280)
568
1106
 
@@ -3659,6 +3659,7 @@ export class DaemonCommandRouter {
3659
3659
  case 'launch_cli':
3660
3660
  case 'stop_cli':
3661
3661
  case 'set_cli_view_mode':
3662
+ case 'record_provider_pty':
3662
3663
  case 'agent_command': {
3663
3664
  return this.deps.cliManager.handleCliCommand(cmd, args);
3664
3665
  }
package/src/index.ts CHANGED
@@ -455,3 +455,30 @@ export type { ExtensionInfo as InstallerExtensionInfo } from './installer.js';
455
455
  // ── Boot / Lifecycle ──
456
456
  export { initDaemonComponents, startDaemonDevSupport, shutdownDaemonComponents } from './boot/daemon-lifecycle.js';
457
457
  export type { DaemonInitConfig, DaemonComponents, DaemonDevSupportOptions } from './boot/daemon-lifecycle.js';
458
+
459
+ // ── Local IPC server (shared between cloud + standalone daemons) ──
460
+ export {
461
+ startLocalIpcServer,
462
+ buildIpcStatusHttpResponse,
463
+ type LocalIpcServerOptions,
464
+ type LocalIpcServerHandle,
465
+ type IpcCommandContext,
466
+ type IpcCommandResult,
467
+ type IpcStatusPayload,
468
+ } from './ipc/local-ipc-server.js';
469
+
470
+ // ── Provider SDK (v1) — selective re-exports for external tooling ──
471
+ // Tooling (registry publish, dashboard validators, the e2e harness) needs
472
+ // the manifest validator, the builder catalog, and the contract version.
473
+ // We don't re-export *everything* from the SDK to keep the public surface
474
+ // stable; consumers that need internal SDK types still import from the
475
+ // sdk/v1 subpath.
476
+ export {
477
+ validateCliProviderManifest,
478
+ formatManifestValidationIssues,
479
+ type ManifestValidationIssue,
480
+ type ManifestValidationResult,
481
+ V1_CONTRACT_VERSION,
482
+ V1_PRIMITIVE_CATALOG,
483
+ V1_ALL_PRIMITIVES,
484
+ } from './providers/sdk/v1/index.js';