productfoo 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +338 -2
- package/bin/foo +3 -0
- package/bin/foo.mjs +42 -0
- package/commands/bug.md +31 -0
- package/commands/discovery.md +28 -0
- package/commands/ideate.md +27 -0
- package/commands/init.md +28 -0
- package/commands/pitch.md +27 -0
- package/commands/preview.md +25 -0
- package/commands/research.md +31 -0
- package/commands/roast.md +27 -0
- package/commands/spec.md +30 -0
- package/config/pricing.yaml +198 -0
- package/config/routing-policy.yaml +171 -0
- package/dist-public/src/activeModel.d.ts +83 -0
- package/dist-public/src/activeModel.js +331 -0
- package/dist-public/src/agentAdapters.d.ts +122 -0
- package/dist-public/src/agentAdapters.js +178 -0
- package/dist-public/src/agentState.d.ts +43 -0
- package/dist-public/src/agentState.js +177 -0
- package/dist-public/src/bugCapsule.d.ts +94 -0
- package/dist-public/src/bugCapsule.js +227 -0
- package/dist-public/src/bugCapture.d.ts +26 -0
- package/dist-public/src/bugCapture.js +251 -0
- package/dist-public/src/bugOwnerLog.d.ts +93 -0
- package/dist-public/src/bugOwnerLog.js +304 -0
- package/dist-public/src/bugReportStore.d.ts +48 -0
- package/dist-public/src/bugReportStore.js +860 -0
- package/dist-public/src/bugSpecContext.d.ts +7 -0
- package/dist-public/src/bugSpecContext.js +129 -0
- package/dist-public/src/channelFreshness.d.ts +25 -0
- package/dist-public/src/channelFreshness.js +194 -0
- package/dist-public/src/cli.d.ts +30 -0
- package/dist-public/src/cli.js +1667 -0
- package/dist-public/src/cliArgs.d.ts +7 -0
- package/dist-public/src/cliArgs.js +44 -0
- package/dist-public/src/codeCatalog.d.ts +41 -0
- package/dist-public/src/codeCatalog.js +197 -0
- package/dist-public/src/commandContract.d.ts +84 -0
- package/dist-public/src/commandContract.js +390 -0
- package/dist-public/src/commandUsage.d.ts +3 -0
- package/dist-public/src/commandUsage.js +30 -0
- package/dist-public/src/commands/agent.d.ts +60 -0
- package/dist-public/src/commands/agent.js +1353 -0
- package/dist-public/src/commands/asp.d.ts +8 -0
- package/dist-public/src/commands/asp.js +669 -0
- package/dist-public/src/commands/evalCmd.d.ts +4 -0
- package/dist-public/src/commands/evalCmd.js +405 -0
- package/dist-public/src/commands/login.d.ts +25 -0
- package/dist-public/src/commands/login.js +94 -0
- package/dist-public/src/commands/loop.d.ts +16 -0
- package/dist-public/src/commands/loop.js +619 -0
- package/dist-public/src/commands/reconcile.d.ts +41 -0
- package/dist-public/src/commands/reconcile.js +292 -0
- package/dist-public/src/commands/runtime.d.ts +1 -0
- package/dist-public/src/commands/runtime.js +36 -0
- package/dist-public/src/commands/scanRedact.d.ts +8 -0
- package/dist-public/src/commands/scanRedact.js +266 -0
- package/dist-public/src/commands/setup.d.ts +28 -0
- package/dist-public/src/commands/setup.js +398 -0
- package/dist-public/src/commands/shared.d.ts +17 -0
- package/dist-public/src/commands/shared.js +147 -0
- package/dist-public/src/commands/source.d.ts +1 -0
- package/dist-public/src/commands/source.js +134 -0
- package/dist-public/src/commands/specCmd.d.ts +24 -0
- package/dist-public/src/commands/specCmd.js +794 -0
- package/dist-public/src/commands/update.d.ts +61 -0
- package/dist-public/src/commands/update.js +244 -0
- package/dist-public/src/comprehensionSignal.d.ts +20 -0
- package/dist-public/src/comprehensionSignal.js +166 -0
- package/dist-public/src/conductor/diversity.d.ts +48 -0
- package/dist-public/src/conductor/diversity.js +423 -0
- package/dist-public/src/conductor/engine.d.ts +7 -0
- package/dist-public/src/conductor/engine.js +295 -0
- package/dist-public/src/conductor/families/ideate.d.ts +2 -0
- package/dist-public/src/conductor/families/ideate.js +255 -0
- package/dist-public/src/conductor/families/interview.d.ts +2 -0
- package/dist-public/src/conductor/families/interview.js +39 -0
- package/dist-public/src/conductor/families/specSwarm.d.ts +2 -0
- package/dist-public/src/conductor/families/specSwarm.js +146 -0
- package/dist-public/src/conductor/familyDelivery.d.ts +6 -0
- package/dist-public/src/conductor/familyDelivery.js +37 -0
- package/dist-public/src/conductor/familyPackArtifact.d.ts +4 -0
- package/dist-public/src/conductor/familyPackArtifact.js +260 -0
- package/dist-public/src/conductor/ideateDecision.d.ts +6 -0
- package/dist-public/src/conductor/ideateDecision.js +237 -0
- package/dist-public/src/conductor/pack.d.ts +26 -0
- package/dist-public/src/conductor/pack.js +75 -0
- package/dist-public/src/conductor/types.d.ts +111 -0
- package/dist-public/src/conductor/types.js +7 -0
- package/dist-public/src/configPath.d.ts +2 -0
- package/dist-public/src/configPath.js +7 -0
- package/dist-public/src/discoveryArtifacts.d.ts +54 -0
- package/dist-public/src/discoveryArtifacts.js +467 -0
- package/dist-public/src/discoveryCommand.d.ts +127 -0
- package/dist-public/src/discoveryCommand.js +2148 -0
- package/dist-public/src/discoveryConnections.d.ts +339 -0
- package/dist-public/src/discoveryConnections.js +613 -0
- package/dist-public/src/discoveryDocs.d.ts +129 -0
- package/dist-public/src/discoveryDocs.js +577 -0
- package/dist-public/src/discoveryEvidence.d.ts +59 -0
- package/dist-public/src/discoveryEvidence.js +198 -0
- package/dist-public/src/discoveryFireflies.d.ts +25 -0
- package/dist-public/src/discoveryFireflies.js +551 -0
- package/dist-public/src/discoveryGmail.d.ts +12 -0
- package/dist-public/src/discoveryGmail.js +466 -0
- package/dist-public/src/discoveryGong.d.ts +12 -0
- package/dist-public/src/discoveryGong.js +491 -0
- package/dist-public/src/discoveryLearningRouting.d.ts +90 -0
- package/dist-public/src/discoveryLearningRouting.js +405 -0
- package/dist-public/src/discoveryMatching.d.ts +26 -0
- package/dist-public/src/discoveryMatching.js +73 -0
- package/dist-public/src/discoveryOutlook.d.ts +12 -0
- package/dist-public/src/discoveryOutlook.js +492 -0
- package/dist-public/src/discoveryProcess.d.ts +221 -0
- package/dist-public/src/discoveryProcess.js +703 -0
- package/dist-public/src/discoveryProof.d.ts +143 -0
- package/dist-public/src/discoveryProof.js +529 -0
- package/dist-public/src/discoveryProviders.d.ts +152 -0
- package/dist-public/src/discoveryProviders.js +756 -0
- package/dist-public/src/discoveryReadiness.d.ts +219 -0
- package/dist-public/src/discoveryReadiness.js +672 -0
- package/dist-public/src/discoveryReconcile.d.ts +57 -0
- package/dist-public/src/discoveryReconcile.js +424 -0
- package/dist-public/src/discoverySlack.d.ts +12 -0
- package/dist-public/src/discoverySlack.js +452 -0
- package/dist-public/src/discoverySources.d.ts +56 -0
- package/dist-public/src/discoverySources.js +322 -0
- package/dist-public/src/discoverySync.d.ts +52 -0
- package/dist-public/src/discoverySync.js +151 -0
- package/dist-public/src/discoverySynthesis.d.ts +57 -0
- package/dist-public/src/discoverySynthesis.js +215 -0
- package/dist-public/src/discoveryTeams.d.ts +12 -0
- package/dist-public/src/discoveryTeams.js +516 -0
- package/dist-public/src/discoveryZoom.d.ts +12 -0
- package/dist-public/src/discoveryZoom.js +523 -0
- package/dist-public/src/elicitationSurface.d.ts +13 -0
- package/dist-public/src/elicitationSurface.js +77 -0
- package/dist-public/src/errorReport.d.ts +99 -0
- package/dist-public/src/errorReport.js +443 -0
- package/dist-public/src/errorTaxonomy.d.ts +84 -0
- package/dist-public/src/errorTaxonomy.js +189 -0
- package/dist-public/src/evalCalibration.d.ts +82 -0
- package/dist-public/src/evalCalibration.js +169 -0
- package/dist-public/src/evalChecks.d.ts +21 -0
- package/dist-public/src/evalChecks.js +295 -0
- package/dist-public/src/evalCompile.d.ts +56 -0
- package/dist-public/src/evalCompile.js +58 -0
- package/dist-public/src/evalImprove.d.ts +60 -0
- package/dist-public/src/evalImprove.js +175 -0
- package/dist-public/src/evalJudge.d.ts +47 -0
- package/dist-public/src/evalJudge.js +143 -0
- package/dist-public/src/evalLabels.d.ts +100 -0
- package/dist-public/src/evalLabels.js +287 -0
- package/dist-public/src/evalRecord.d.ts +68 -0
- package/dist-public/src/evalRecord.js +165 -0
- package/dist-public/src/evalRoast.d.ts +88 -0
- package/dist-public/src/evalRoast.js +249 -0
- package/dist-public/src/evidenceIntake.d.ts +66 -0
- package/dist-public/src/evidenceIntake.js +356 -0
- package/dist-public/src/feedback.d.ts +78 -0
- package/dist-public/src/feedback.js +294 -0
- package/dist-public/src/firefliesClient.d.ts +106 -0
- package/dist-public/src/firefliesClient.js +258 -0
- package/dist-public/src/firstMoveTensions.d.ts +10 -0
- package/dist-public/src/firstMoveTensions.js +93 -0
- package/dist-public/src/gitRun.d.ts +21 -0
- package/dist-public/src/gitRun.js +50 -0
- package/dist-public/src/githubReconcile.d.ts +94 -0
- package/dist-public/src/githubReconcile.js +205 -0
- package/dist-public/src/gmailNango.d.ts +131 -0
- package/dist-public/src/gmailNango.js +329 -0
- package/dist-public/src/gongNango.d.ts +118 -0
- package/dist-public/src/gongNango.js +308 -0
- package/dist-public/src/grammarGate.d.ts +13 -0
- package/dist-public/src/grammarGate.js +72 -0
- package/dist-public/src/harnessGates.d.ts +65 -0
- package/dist-public/src/harnessGates.js +168 -0
- package/dist-public/src/humanSurfaceEval.d.ts +19 -0
- package/dist-public/src/humanSurfaceEval.js +156 -0
- package/dist-public/src/ideate.d.ts +43 -0
- package/dist-public/src/ideate.js +255 -0
- package/dist-public/src/ideateContext.d.ts +11 -0
- package/dist-public/src/ideateContext.js +83 -0
- package/dist-public/src/index.d.ts +2 -0
- package/dist-public/src/index.js +4 -0
- package/dist-public/src/initCommand.d.ts +25 -0
- package/dist-public/src/initCommand.js +558 -0
- package/dist-public/src/interviewGate.d.ts +25 -0
- package/dist-public/src/interviewGate.js +126 -0
- package/dist-public/src/jsonFile.d.ts +2 -0
- package/dist-public/src/jsonFile.js +22 -0
- package/dist-public/src/loopBudget.d.ts +182 -0
- package/dist-public/src/loopBudget.js +494 -0
- package/dist-public/src/loopContracts.d.ts +35 -0
- package/dist-public/src/loopContracts.js +364 -0
- package/dist-public/src/machineState.d.ts +1 -0
- package/dist-public/src/machineState.js +9 -0
- package/dist-public/src/notionNango.d.ts +93 -0
- package/dist-public/src/notionNango.js +279 -0
- package/dist-public/src/orchestrationResult.d.ts +11 -0
- package/dist-public/src/orchestrationResult.js +73 -0
- package/dist-public/src/outlookNango.d.ts +124 -0
- package/dist-public/src/outlookNango.js +327 -0
- package/dist-public/src/packetCorrections.d.ts +40 -0
- package/dist-public/src/packetCorrections.js +172 -0
- package/dist-public/src/packetGlossary.d.ts +6 -0
- package/dist-public/src/packetGlossary.js +42 -0
- package/dist-public/src/packetIndex.d.ts +54 -0
- package/dist-public/src/packetIndex.js +744 -0
- package/dist-public/src/packetIndexEntry.d.ts +3 -0
- package/dist-public/src/packetIndexEntry.js +128 -0
- package/dist-public/src/packetIndexServe.d.ts +13 -0
- package/dist-public/src/packetIndexServe.js +113 -0
- package/dist-public/src/packetIndexWorker.d.ts +7 -0
- package/dist-public/src/packetIndexWorker.js +45 -0
- package/dist-public/src/packetLifecycle.d.ts +30 -0
- package/dist-public/src/packetLifecycle.js +127 -0
- package/dist-public/src/packetReconcile.d.ts +73 -0
- package/dist-public/src/packetReconcile.js +386 -0
- package/dist-public/src/packetStatus.d.ts +125 -0
- package/dist-public/src/packetStatus.js +770 -0
- package/dist-public/src/packetValidation.d.ts +17 -0
- package/dist-public/src/packetValidation.js +333 -0
- package/dist-public/src/packetVerify.d.ts +73 -0
- package/dist-public/src/packetVerify.js +303 -0
- package/dist-public/src/packetWrite.d.ts +52 -0
- package/dist-public/src/packetWrite.js +448 -0
- package/dist-public/src/pitch.d.ts +38 -0
- package/dist-public/src/pitch.js +154 -0
- package/dist-public/src/processMemo.d.ts +8 -0
- package/dist-public/src/processMemo.js +23 -0
- package/dist-public/src/productDetect.d.ts +30 -0
- package/dist-public/src/productDetect.js +376 -0
- package/dist-public/src/profile.d.ts +62 -0
- package/dist-public/src/profile.js +334 -0
- package/dist-public/src/questionModel.d.ts +46 -0
- package/dist-public/src/questionModel.js +74 -0
- package/dist-public/src/questionText.d.ts +7 -0
- package/dist-public/src/questionText.js +38 -0
- package/dist-public/src/redactConfig.d.ts +2 -0
- package/dist-public/src/redactConfig.js +11 -0
- package/dist-public/src/redactEngine.d.ts +59 -0
- package/dist-public/src/redactEngine.js +509 -0
- package/dist-public/src/redactGate.d.ts +63 -0
- package/dist-public/src/redactGate.js +209 -0
- package/dist-public/src/repoRead.d.ts +29 -0
- package/dist-public/src/repoRead.js +160 -0
- package/dist-public/src/research.d.ts +242 -0
- package/dist-public/src/research.js +1314 -0
- package/dist-public/src/researchCompletion.d.ts +34 -0
- package/dist-public/src/researchCompletion.js +123 -0
- package/dist-public/src/researchExecution.d.ts +47 -0
- package/dist-public/src/researchExecution.js +309 -0
- package/dist-public/src/researchFormat.d.ts +21 -0
- package/dist-public/src/researchFormat.js +77 -0
- package/dist-public/src/researchGate.d.ts +86 -0
- package/dist-public/src/researchGate.js +1036 -0
- package/dist-public/src/researchReceipt.d.ts +76 -0
- package/dist-public/src/researchReceipt.js +148 -0
- package/dist-public/src/researchRender.d.ts +2 -0
- package/dist-public/src/researchRender.js +267 -0
- package/dist-public/src/researchSigningKey.d.ts +19 -0
- package/dist-public/src/researchSigningKey.js +95 -0
- package/dist-public/src/riskClassify.d.ts +42 -0
- package/dist-public/src/riskClassify.js +149 -0
- package/dist-public/src/routines.d.ts +42 -0
- package/dist-public/src/routines.js +269 -0
- package/dist-public/src/routingPolicy.d.ts +172 -0
- package/dist-public/src/routingPolicy.js +646 -0
- package/dist-public/src/secretInput.d.ts +8 -0
- package/dist-public/src/secretInput.js +89 -0
- package/dist-public/src/selfCheck.d.ts +7 -0
- package/dist-public/src/selfCheck.js +473 -0
- package/dist-public/src/selfCheckCache.d.ts +29 -0
- package/dist-public/src/selfCheckCache.js +147 -0
- package/dist-public/src/severityClass.d.ts +15 -0
- package/dist-public/src/severityClass.js +22 -0
- package/dist-public/src/skillClientPolicy.d.ts +8 -0
- package/dist-public/src/skillClientPolicy.js +16 -0
- package/dist-public/src/skillLocal.d.ts +98 -0
- package/dist-public/src/skillLocal.js +463 -0
- package/dist-public/src/skillTreeDigest.d.ts +9 -0
- package/dist-public/src/skillTreeDigest.js +64 -0
- package/dist-public/src/slackNango.d.ts +62 -0
- package/dist-public/src/slackNango.js +208 -0
- package/dist-public/src/social/arxiv.d.ts +19 -0
- package/dist-public/src/social/arxiv.js +96 -0
- package/dist-public/src/social/github.d.ts +2 -0
- package/dist-public/src/social/github.js +78 -0
- package/dist-public/src/social/hackernews.d.ts +2 -0
- package/dist-public/src/social/hackernews.js +67 -0
- package/dist-public/src/social/index.d.ts +10 -0
- package/dist-public/src/social/index.js +79 -0
- package/dist-public/src/social/reddit.d.ts +2 -0
- package/dist-public/src/social/reddit.js +125 -0
- package/dist-public/src/social/types.d.ts +73 -0
- package/dist-public/src/social/types.js +61 -0
- package/dist-public/src/social/xml.d.ts +10 -0
- package/dist-public/src/social/xml.js +49 -0
- package/dist-public/src/social/youtube.d.ts +7 -0
- package/dist-public/src/social/youtube.js +217 -0
- package/dist-public/src/sourceIngress.d.ts +488 -0
- package/dist-public/src/sourceIngress.js +1651 -0
- package/dist-public/src/specAfter.d.ts +187 -0
- package/dist-public/src/specAfter.js +814 -0
- package/dist-public/src/specBudget.d.ts +124 -0
- package/dist-public/src/specBudget.js +424 -0
- package/dist-public/src/specDecisionSignals.d.ts +38 -0
- package/dist-public/src/specDecisionSignals.js +121 -0
- package/dist-public/src/specLinkedCommits.d.ts +7 -0
- package/dist-public/src/specLinkedCommits.js +107 -0
- package/dist-public/src/specSections.d.ts +105 -0
- package/dist-public/src/specSections.js +718 -0
- package/dist-public/src/statusGlyphs.d.ts +18 -0
- package/dist-public/src/statusGlyphs.js +75 -0
- package/dist-public/src/swarmCommand.d.ts +10 -0
- package/dist-public/src/swarmCommand.js +96 -0
- package/dist-public/src/teamsNango.d.ts +114 -0
- package/dist-public/src/teamsNango.js +301 -0
- package/dist-public/src/tokenStore.d.ts +10 -0
- package/dist-public/src/tokenStore.js +77 -0
- package/dist-public/src/trustEnvelope.d.ts +78 -0
- package/dist-public/src/trustEnvelope.js +392 -0
- package/dist-public/src/untrustedText.d.ts +6 -0
- package/dist-public/src/untrustedText.js +20 -0
- package/dist-public/src/updateNotice.d.ts +41 -0
- package/dist-public/src/updateNotice.js +290 -0
- package/dist-public/src/vendorYaml.d.ts +1 -0
- package/dist-public/src/vendorYaml.js +7374 -0
- package/dist-public/src/workflowRunActuals.d.ts +67 -0
- package/dist-public/src/workflowRunActuals.js +184 -0
- package/dist-public/src/zendeskDiscovery.d.ts +12 -0
- package/dist-public/src/zendeskDiscovery.js +513 -0
- package/dist-public/src/zendeskNango.d.ts +131 -0
- package/dist-public/src/zendeskNango.js +344 -0
- package/dist-public/src/zoomNango.d.ts +106 -0
- package/dist-public/src/zoomNango.js +351 -0
- package/docs/CHANGELOG.md +32 -0
- package/docs/THIRD_PARTY_NOTICES.md +36 -0
- package/package.json +71 -4
- package/schemas/bug-capsule-receipt.schema.json +54 -0
- package/schemas/bug-capsule.schema.json +189 -0
- package/schemas/discovery-evidence-envelope.schema.json +79 -0
- package/schemas/discovery-processing-receipt.schema.json +71 -0
- package/schemas/discovery-service-contract.schema.json +189 -0
- package/skills/AGENTS.md +32 -0
- package/skills/README.md +287 -0
- package/skills/bug/SKILL.md +49 -0
- package/skills/build-preview-spec/SKILL.md +103 -0
- package/skills/capture-external-artifact/SKILL.md +23 -0
- package/skills/capture-external-artifact/agents/openai.yaml +4 -0
- package/skills/capture-external-artifact/references/extractor-libraries.md +27 -0
- package/skills/check-spec-health/SKILL.md +45 -0
- package/skills/check-spec-health/agents/openai.yaml +4 -0
- package/skills/competitive-positioning/SKILL.md +86 -0
- package/skills/competitive-positioning/agents/openai.yaml +4 -0
- package/skills/cpo-lens/SKILL.md +398 -0
- package/skills/cpo-lens/agents/openai.yaml +4 -0
- package/skills/create-prd/SKILL.md +26 -0
- package/skills/create-prd/agents/openai.yaml +4 -0
- package/skills/customer-discovery/SKILL.md +192 -0
- package/skills/design-system/SKILL.md +49 -0
- package/skills/design-system/agents/openai.yaml +4 -0
- package/skills/discussion-guide/SKILL.md +75 -0
- package/skills/discussion-guide/agents/openai.yaml +4 -0
- package/skills/firecrawl-research-papers/SKILL.md +96 -0
- package/skills/firecrawl-research-papers/agents/openai.yaml +4 -0
- package/skills/foo-roast/SKILL.md +325 -0
- package/skills/generate-gtm-brief/SKILL.md +41 -0
- package/skills/generate-gtm-brief/agents/openai.yaml +4 -0
- package/skills/generate-gtm-brief/references/gtm-brief-template.md +56 -0
- package/skills/gong-quotes/SKILL.md +43 -0
- package/skills/gong-quotes/agents/openai.yaml +4 -0
- package/skills/grill-me/SKILL.md +27 -0
- package/skills/grill-me/agents/openai.yaml +4 -0
- package/skills/ideate/SKILL.md +180 -0
- package/skills/ideate/references/family-packs.md +5 -0
- package/skills/initiative-tracker/SKILL.md +23 -0
- package/skills/initiative-tracker/agents/openai.yaml +4 -0
- package/skills/interop-review/SKILL.md +25 -0
- package/skills/interop-review/agents/openai.yaml +4 -0
- package/skills/interview-me/SKILL.md +404 -0
- package/skills/interview-me/agents/openai.yaml +4 -0
- package/skills/loop-library-audit/SKILL.md +123 -0
- package/skills/pitch/SKILL.md +110 -0
- package/skills/pitch/references/opportunity-assessment.md +136 -0
- package/skills/pitch/references/prfaq-template.md +54 -0
- package/skills/pmm-release-briefing/SKILL.md +27 -0
- package/skills/pmm-release-briefing/agents/openai.yaml +4 -0
- package/skills/pmm-release-briefing/references/variant-generalized.md +16 -0
- package/skills/positioning-messaging/SKILL.md +70 -0
- package/skills/positioning-messaging/agents/openai.yaml +4 -0
- package/skills/positioning-messaging/references/guest-insights.md +1400 -0
- package/skills/preview-swarm/SKILL.md +418 -0
- package/skills/preview-swarm/agents/openai.yaml +4 -0
- package/skills/preview-swarm/references/html-effectiveness-build-previews.md +168 -0
- package/skills/product-copywriting/SKILL.md +127 -0
- package/skills/product-copywriting/agents/openai.yaml +4 -0
- package/skills/product-copywriting/references/foo-voice.md +26 -0
- package/skills/product-shape-interview/SKILL.md +219 -0
- package/skills/product-shape-interview/agents/openai.yaml +4 -0
- package/skills/ready-for-release/SKILL.md +39 -0
- package/skills/ready-for-release/agents/openai.yaml +4 -0
- package/skills/redact-transcripts/SKILL.md +23 -0
- package/skills/redact-transcripts/agents/openai.yaml +4 -0
- package/skills/research-brief/SKILL.md +48 -0
- package/skills/research-brief/agents/openai.yaml +4 -0
- package/skills/research-refresh/SKILL.md +63 -0
- package/skills/research-refresh/references/worked-cases.md +44 -0
- package/skills/review-loop/SKILL.md +200 -0
- package/skills/review-spec/SKILL.md +32 -0
- package/skills/review-spec/agents/openai.yaml +4 -0
- package/skills/rfc-from-research/SKILL.md +42 -0
- package/skills/rfc-from-research/agents/openai.yaml +4 -0
- package/skills/rfc-from-research/references/rfc-pattern.md +69 -0
- package/skills/skill-registry.yaml +810 -0
- package/skills/spec-cleanup/SKILL.md +103 -0
- package/skills/spec-cleanup/agents/openai.yaml +4 -0
- package/skills/spec-swarm/SKILL.md +87 -0
- package/skills/spec-swarm/agents/openai.yaml +4 -0
- package/skills/spec-swarm/template/SKILL.template.md +94 -0
- package/skills/spec-swarm/template/personas/accessibility-inclusion.md +40 -0
- package/skills/spec-swarm/template/personas/ai-technical-pm.md +69 -0
- package/skills/spec-swarm/template/personas/analytics-engineering.md +39 -0
- package/skills/spec-swarm/template/personas/applied-ai-eval.md +64 -0
- package/skills/spec-swarm/template/personas/business-gm-pm.md +67 -0
- package/skills/spec-swarm/template/personas/consumer-craft-pm.md +68 -0
- package/skills/spec-swarm/template/personas/content-design.md +40 -0
- package/skills/spec-swarm/template/personas/cpo.md +42 -0
- package/skills/spec-swarm/template/personas/data-science.md +42 -0
- package/skills/spec-swarm/template/personas/developer-experience.md +65 -0
- package/skills/spec-swarm/template/personas/dx-product.md +70 -0
- package/skills/spec-swarm/template/personas/field.md +43 -0
- package/skills/spec-swarm/template/personas/finance-cfo.md +40 -0
- package/skills/spec-swarm/template/personas/growth-pm.md +67 -0
- package/skills/spec-swarm/template/personas/platform-pm.md +67 -0
- package/skills/spec-swarm/template/personas/pmm.md +43 -0
- package/skills/spec-swarm/template/personas/principal-product-designer.md +42 -0
- package/skills/spec-swarm/template/personas/product-ops-program.md +41 -0
- package/skills/spec-swarm/template/personas/qa-release.md +41 -0
- package/skills/spec-swarm/template/personas/responsible-ai-safety.md +64 -0
- package/skills/spec-swarm/template/personas/security-privacy-legal.md +40 -0
- package/skills/spec-swarm/template/personas/strategy-competitive.md +41 -0
- package/skills/spec-swarm/template/personas/technical-architect.md +42 -0
- package/skills/spec-swarm/template/personas/technical-lead.md +43 -0
- package/skills/spec-swarm/template/personas/user-research.md +41 -0
- package/skills/spec-swarm-accessibility-inclusion/SKILL.md +95 -0
- package/skills/spec-swarm-accessibility-inclusion/agents/openai.yaml +4 -0
- package/skills/spec-swarm-ai-technical-pm/SKILL.md +124 -0
- package/skills/spec-swarm-analytics-engineering/SKILL.md +94 -0
- package/skills/spec-swarm-analytics-engineering/agents/openai.yaml +4 -0
- package/skills/spec-swarm-applied-ai-eval/SKILL.md +120 -0
- package/skills/spec-swarm-business-gm-pm/SKILL.md +122 -0
- package/skills/spec-swarm-consumer-craft-pm/SKILL.md +122 -0
- package/skills/spec-swarm-content-design/SKILL.md +95 -0
- package/skills/spec-swarm-content-design/agents/openai.yaml +4 -0
- package/skills/spec-swarm-cpo/SKILL.md +94 -0
- package/skills/spec-swarm-cpo/agents/openai.yaml +4 -0
- package/skills/spec-swarm-data-science/SKILL.md +95 -0
- package/skills/spec-swarm-data-science/agents/openai.yaml +4 -0
- package/skills/spec-swarm-developer-experience/SKILL.md +121 -0
- package/skills/spec-swarm-dx-product/SKILL.md +126 -0
- package/skills/spec-swarm-field/SKILL.md +96 -0
- package/skills/spec-swarm-field/agents/openai.yaml +4 -0
- package/skills/spec-swarm-finance-cfo/SKILL.md +95 -0
- package/skills/spec-swarm-finance-cfo/agents/openai.yaml +4 -0
- package/skills/spec-swarm-growth-pm/SKILL.md +122 -0
- package/skills/spec-swarm-platform-pm/SKILL.md +122 -0
- package/skills/spec-swarm-pmm/SKILL.md +96 -0
- package/skills/spec-swarm-pmm/agents/openai.yaml +4 -0
- package/skills/spec-swarm-principal-product-designer/SKILL.md +95 -0
- package/skills/spec-swarm-principal-product-designer/agents/openai.yaml +4 -0
- package/skills/spec-swarm-product-ops-program/SKILL.md +96 -0
- package/skills/spec-swarm-product-ops-program/agents/openai.yaml +4 -0
- package/skills/spec-swarm-qa-release/SKILL.md +96 -0
- package/skills/spec-swarm-qa-release/agents/openai.yaml +4 -0
- package/skills/spec-swarm-responsible-ai-safety/SKILL.md +120 -0
- package/skills/spec-swarm-security-privacy-legal/SKILL.md +95 -0
- package/skills/spec-swarm-security-privacy-legal/agents/openai.yaml +4 -0
- package/skills/spec-swarm-strategy-competitive/SKILL.md +96 -0
- package/skills/spec-swarm-strategy-competitive/agents/openai.yaml +4 -0
- package/skills/spec-swarm-technical-architect/SKILL.md +95 -0
- package/skills/spec-swarm-technical-architect/agents/openai.yaml +4 -0
- package/skills/spec-swarm-technical-lead/SKILL.md +96 -0
- package/skills/spec-swarm-technical-lead/agents/openai.yaml +4 -0
- package/skills/spec-swarm-user-research/SKILL.md +95 -0
- package/skills/spec-swarm-user-research/agents/openai.yaml +4 -0
- package/skills/strategy-check/SKILL.md +170 -0
- package/skills/strategy-check/agents/openai.yaml +4 -0
- package/skills/study-digest/SKILL.md +41 -0
- package/skills/synthetic-customer/SKILL.md +68 -0
- package/skills/update-cli-spec/SKILL.md +69 -0
- package/skills/update-cli-spec/agents/openai.yaml +4 -0
- package/skills/voc-researcher/SKILL.md +45 -0
- package/skills/voc-researcher/agents/openai.yaml +4 -0
- package/skills-lock.json +35 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Reddit adapter — the one non-trivial port. Bare reddit.com/*.json rate-limits
|
|
2
|
+
// hard (429/403), which is why last30days ships several strategies.
|
|
3
|
+
//
|
|
4
|
+
// HONEST LIMITATION (found by the dependency watcher, 2026-07-12): Arctic Shift
|
|
5
|
+
// CANNOT serve a global keyword search — its API answers
|
|
6
|
+
// "'query' query parameter requires one of: author, subreddit"
|
|
7
|
+
// so an arctic call with only a query returns HTTP 400, always. It was in this
|
|
8
|
+
// chain and had never once worked; the fallback masked it. It is removed rather
|
|
9
|
+
// than left as a strategy that silently fails. (It does work when scoped to a
|
|
10
|
+
// subreddit — see OQ: use it for subreddit-scoped search, which would restore an
|
|
11
|
+
// off-reddit.com fallback.)
|
|
12
|
+
//
|
|
13
|
+
// So the chain is Reddit Atom (.rss) → Reddit public JSON. Both live on
|
|
14
|
+
// reddit.com and therefore throttle TOGETHER: there is currently no off-domain
|
|
15
|
+
// fallback for global keyword search. Resilience here is weaker than a
|
|
16
|
+
// multi-host chain, and the spec says so rather than implying otherwise. A
|
|
17
|
+
// strategy that throws or 429s drops to the next; the source is only `excluded`
|
|
18
|
+
// if all fail (R-001 done_when).
|
|
19
|
+
//
|
|
20
|
+
// Reddit asks clients to send a descriptive User-Agent; we do (it is correct
|
|
21
|
+
// practice, though it does not by itself defeat rate limiting).
|
|
22
|
+
import { resolveOptions, settleStatus, withinWindow } from "./types.js";
|
|
23
|
+
import { parseAtomEntries } from "./xml.js";
|
|
24
|
+
function postToItem(p) {
|
|
25
|
+
if (!p.id || !p.title)
|
|
26
|
+
return null;
|
|
27
|
+
const createdSec = typeof p.created_utc === "number" ? p.created_utc : 0;
|
|
28
|
+
return {
|
|
29
|
+
source: "reddit",
|
|
30
|
+
id: `reddit:${p.id}`,
|
|
31
|
+
title: p.title,
|
|
32
|
+
url: p.permalink ? `https://www.reddit.com${p.permalink}` : p.url || "",
|
|
33
|
+
author: p.author ?? null,
|
|
34
|
+
createdAt: new Date(createdSec * 1000).toISOString(),
|
|
35
|
+
score: typeof p.score === "number" ? p.score : null,
|
|
36
|
+
snippet: p.selftext ? p.selftext.slice(0, 280) : null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function filterAndCap(items, o) {
|
|
40
|
+
return items
|
|
41
|
+
.filter((it) => it !== null && Boolean(it.url))
|
|
42
|
+
.filter((it) => withinWindow(it.createdAt, o.now, o.days))
|
|
43
|
+
.slice(0, o.limit);
|
|
44
|
+
}
|
|
45
|
+
// Reddit asks for a unique, descriptive User-Agent on API-ish requests.
|
|
46
|
+
const REDDIT_UA = "foo-research/0.1 (+https://github.com/rwo572/productfoo)";
|
|
47
|
+
async function redditRss(query, o) {
|
|
48
|
+
const url = `https://www.reddit.com/search.rss?q=${encodeURIComponent(query)}&sort=relevance&t=month`;
|
|
49
|
+
const res = await o.fetchImpl(url, {
|
|
50
|
+
headers: { Accept: "application/atom+xml", "User-Agent": REDDIT_UA },
|
|
51
|
+
signal: o.signal,
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok)
|
|
54
|
+
throw new Error(`HTTP ${res.status}`);
|
|
55
|
+
const xml = await res.text();
|
|
56
|
+
const items = parseAtomEntries(xml).map((e) => {
|
|
57
|
+
if (!e.title || !e.link)
|
|
58
|
+
return null;
|
|
59
|
+
// Reddit Atom ids look like "t3_abcdef"; fall back to the link.
|
|
60
|
+
const id = e.id ? e.id.replace(/^.*?(t3_[a-z0-9]+).*$/i, "$1") : e.link;
|
|
61
|
+
return {
|
|
62
|
+
source: "reddit",
|
|
63
|
+
id: `reddit:${id}`,
|
|
64
|
+
title: e.title,
|
|
65
|
+
url: e.link,
|
|
66
|
+
author: e.author,
|
|
67
|
+
createdAt: e.published ?? new Date(0).toISOString(),
|
|
68
|
+
score: null, // the Atom feed carries no engagement score
|
|
69
|
+
snippet: e.summary ? e.summary.slice(0, 280) : null,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
return filterAndCap(items, o);
|
|
73
|
+
}
|
|
74
|
+
async function redditPublicJson(query, o) {
|
|
75
|
+
const url = `https://www.reddit.com/search.json?q=${encodeURIComponent(query)}` +
|
|
76
|
+
`&sort=relevance&t=month&limit=${o.limit}`;
|
|
77
|
+
const res = await o.fetchImpl(url, {
|
|
78
|
+
headers: { Accept: "application/json", "User-Agent": REDDIT_UA },
|
|
79
|
+
signal: o.signal,
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok)
|
|
82
|
+
throw new Error(`HTTP ${res.status}`);
|
|
83
|
+
const body = (await res.json());
|
|
84
|
+
const children = body.data?.children ?? [];
|
|
85
|
+
return filterAndCap(children.map((c) => (c.data ? postToItem(c.data) : null)), o);
|
|
86
|
+
}
|
|
87
|
+
// arctic is deliberately absent: its API cannot do a global keyword search, so
|
|
88
|
+
// it returned HTTP 400 on every call (see the header note). Both remaining
|
|
89
|
+
// strategies are on reddit.com and throttle together — a known, recorded
|
|
90
|
+
// resilience gap, not a silent one.
|
|
91
|
+
const STRATEGIES = [
|
|
92
|
+
{ name: "rss", run: redditRss },
|
|
93
|
+
{ name: "public", run: redditPublicJson },
|
|
94
|
+
];
|
|
95
|
+
export async function fetchReddit(query, opts = {}) {
|
|
96
|
+
const o = resolveOptions(opts);
|
|
97
|
+
const tried = [];
|
|
98
|
+
for (const strategy of STRATEGIES) {
|
|
99
|
+
try {
|
|
100
|
+
const items = await strategy.run(query, o);
|
|
101
|
+
if (items.length > 0) {
|
|
102
|
+
const prior = tried.length ? ` (after ${tried.join(", ")})` : "";
|
|
103
|
+
return {
|
|
104
|
+
source: "reddit",
|
|
105
|
+
status: settleStatus("available", items.length),
|
|
106
|
+
reason: `Reddit via ${strategy.name}: ${items.length} post(s) within ${o.days}d${prior}.`,
|
|
107
|
+
items,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
tried.push(`${strategy.name}: empty`);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
tried.push(`${strategy.name}: ${err.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Every strategy either failed or returned nothing. If any strategy actually
|
|
117
|
+
// ran without error we degraded (no signal); if all threw we exclude.
|
|
118
|
+
const allErrored = tried.every((t) => !t.endsWith("empty"));
|
|
119
|
+
return {
|
|
120
|
+
source: "reddit",
|
|
121
|
+
status: allErrored ? "excluded" : "degraded",
|
|
122
|
+
reason: `Reddit returned no usable posts — ${tried.join("; ")}.`,
|
|
123
|
+
items: [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type SocialSource = "reddit" | "hackernews" | "arxiv" | "github" | "youtube";
|
|
2
|
+
export interface YtVideo {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
url: string;
|
|
6
|
+
uploader: string | null;
|
|
7
|
+
uploadDate: string;
|
|
8
|
+
viewCount: number | null;
|
|
9
|
+
}
|
|
10
|
+
export interface YtDlp {
|
|
11
|
+
search(query: string, n: number): Promise<YtVideo[]>;
|
|
12
|
+
transcript?(videoId: string): Promise<string | null>;
|
|
13
|
+
}
|
|
14
|
+
export type AdapterStatus = "available" | "empty" | "degraded" | "excluded";
|
|
15
|
+
export interface SocialItem {
|
|
16
|
+
source: SocialSource;
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
url: string;
|
|
20
|
+
author: string | null;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
score: number | null;
|
|
23
|
+
snippet: string | null;
|
|
24
|
+
}
|
|
25
|
+
export interface AdapterResult {
|
|
26
|
+
source: SocialSource;
|
|
27
|
+
status: AdapterStatus;
|
|
28
|
+
reason: string;
|
|
29
|
+
items: SocialItem[];
|
|
30
|
+
}
|
|
31
|
+
export interface HttpResponse {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
status: number;
|
|
34
|
+
json(): Promise<unknown>;
|
|
35
|
+
text(): Promise<string>;
|
|
36
|
+
}
|
|
37
|
+
export type FetchLike = (url: string, init?: {
|
|
38
|
+
headers?: Record<string, string>;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}) => Promise<HttpResponse>;
|
|
41
|
+
export interface AdapterOptions {
|
|
42
|
+
days?: number;
|
|
43
|
+
limit?: number;
|
|
44
|
+
now?: Date;
|
|
45
|
+
fetchImpl?: FetchLike;
|
|
46
|
+
githubToken?: string;
|
|
47
|
+
ytdlp?: YtDlp;
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
}
|
|
50
|
+
export declare const DEFAULT_DAYS = 30;
|
|
51
|
+
export declare const DEFAULT_LIMIT = 25;
|
|
52
|
+
export declare const DEFAULT_FETCH_TIMEOUT_MS = 30000;
|
|
53
|
+
export interface ResolvedOptions {
|
|
54
|
+
days: number;
|
|
55
|
+
limit: number;
|
|
56
|
+
now: Date;
|
|
57
|
+
fetchImpl: FetchLike;
|
|
58
|
+
githubToken?: string;
|
|
59
|
+
signal: AbortSignal;
|
|
60
|
+
}
|
|
61
|
+
export declare function resolveOptions(opts?: AdapterOptions): ResolvedOptions;
|
|
62
|
+
/**
|
|
63
|
+
* The honest-zero rule, in one place: a source that returned no items did not
|
|
64
|
+
* contribute evidence, so it cannot report `available` — whatever it thinks.
|
|
65
|
+
*
|
|
66
|
+
* Applied at BOTH layers on purpose. Each adapter settles its own status through
|
|
67
|
+
* this, so an adapter is honest when called directly; and `gatherSocial` re-applies
|
|
68
|
+
* it, so a future adapter that forgets cannot reintroduce the lie. `degraded` and
|
|
69
|
+
* `excluded` already carry their own meaning and pass through untouched.
|
|
70
|
+
*/
|
|
71
|
+
export declare function settleStatus(status: AdapterStatus, itemCount: number): AdapterStatus;
|
|
72
|
+
export declare function windowStartMs(now: Date, days: number): number;
|
|
73
|
+
export declare function withinWindow(iso: string, now: Date, days: number): boolean;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// foo social adapters — the keyless free-tier port of last30days into productfoo's
|
|
2
|
+
// `social` SourceClass (specs/research-social-last30d-v0, R-001). This module is
|
|
3
|
+
// the *fetch* layer: it is deliberately separate from research.ts (the
|
|
4
|
+
// deterministic, fetch-free spine) so the spine keeps its "never fetches"
|
|
5
|
+
// invariant. The agent invokes these adapters at gather time; foo synthesizes.
|
|
6
|
+
//
|
|
7
|
+
// Every adapter is offline-testable: network access is a single injected
|
|
8
|
+
// `FetchLike` (default = global fetch), so unit tests pass fixtures and no live
|
|
9
|
+
// I/O runs in the test suite — the same posture as src/sourceIngress.ts.
|
|
10
|
+
//
|
|
11
|
+
// Every HTTP fetch is also time-bounded: `resolveOptions` hands each adapter one
|
|
12
|
+
// abort signal (default ceiling `DEFAULT_FETCH_TIMEOUT_MS`, overridable with
|
|
13
|
+
// `timeoutMs` for tests and slow-host tuning) that the adapter passes on every
|
|
14
|
+
// call, so one hung endpoint can no longer stall the whole gather.
|
|
15
|
+
export const DEFAULT_DAYS = 30;
|
|
16
|
+
export const DEFAULT_LIMIT = 25;
|
|
17
|
+
// The HTTP fetch ceiling. Before this existed a hung endpoint stalled the whole
|
|
18
|
+
// gather for minutes, bounded only by undici's defaults (#774).
|
|
19
|
+
export const DEFAULT_FETCH_TIMEOUT_MS = 30_000;
|
|
20
|
+
const globalFetch = (url, init) =>
|
|
21
|
+
// eslint-disable-next-line no-undef
|
|
22
|
+
fetch(url, {
|
|
23
|
+
...init,
|
|
24
|
+
// Default ceiling for direct callers; the adapters pass the resolved signal
|
|
25
|
+
// explicitly so the `timeoutMs` override reaches every call.
|
|
26
|
+
signal: init?.signal ?? AbortSignal.timeout(DEFAULT_FETCH_TIMEOUT_MS),
|
|
27
|
+
});
|
|
28
|
+
export function resolveOptions(opts = {}) {
|
|
29
|
+
return {
|
|
30
|
+
days: opts.days ?? DEFAULT_DAYS,
|
|
31
|
+
limit: opts.limit ?? DEFAULT_LIMIT,
|
|
32
|
+
now: opts.now ?? new Date(),
|
|
33
|
+
fetchImpl: opts.fetchImpl ?? globalFetch,
|
|
34
|
+
githubToken: opts.githubToken,
|
|
35
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The honest-zero rule, in one place: a source that returned no items did not
|
|
40
|
+
* contribute evidence, so it cannot report `available` — whatever it thinks.
|
|
41
|
+
*
|
|
42
|
+
* Applied at BOTH layers on purpose. Each adapter settles its own status through
|
|
43
|
+
* this, so an adapter is honest when called directly; and `gatherSocial` re-applies
|
|
44
|
+
* it, so a future adapter that forgets cannot reintroduce the lie. `degraded` and
|
|
45
|
+
* `excluded` already carry their own meaning and pass through untouched.
|
|
46
|
+
*/
|
|
47
|
+
export function settleStatus(status, itemCount) {
|
|
48
|
+
return status === "available" && itemCount === 0 ? "empty" : status;
|
|
49
|
+
}
|
|
50
|
+
// The oldest timestamp (ms) an item may carry to fall inside the recency window.
|
|
51
|
+
export function windowStartMs(now, days) {
|
|
52
|
+
return now.getTime() - days * 24 * 60 * 60 * 1000;
|
|
53
|
+
}
|
|
54
|
+
// True when `iso` parses and is at or after the window start. Unparseable dates
|
|
55
|
+
// are treated as out-of-window (defensive: never let a garbage date leak in).
|
|
56
|
+
export function withinWindow(iso, now, days) {
|
|
57
|
+
const t = Date.parse(iso);
|
|
58
|
+
if (Number.isNaN(t))
|
|
59
|
+
return false;
|
|
60
|
+
return t >= windowStartMs(now, days) && t <= now.getTime() + 60_000;
|
|
61
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface AtomEntry {
|
|
2
|
+
id: string | null;
|
|
3
|
+
title: string | null;
|
|
4
|
+
link: string | null;
|
|
5
|
+
published: string | null;
|
|
6
|
+
author: string | null;
|
|
7
|
+
summary: string | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function unescapeXml(value: string | null): string | null;
|
|
10
|
+
export declare function parseAtomEntries(xml: string): AtomEntry[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// A minimal, dependency-free Atom reader — just enough to parse the two Atom
|
|
2
|
+
// feeds this module consumes (Reddit search.rss and the arXiv query API). It is
|
|
3
|
+
// deliberately lenient (regex over well-formed, regular feeds) rather than a
|
|
4
|
+
// general XML parser; fixtures in the test suite pin the shapes it must handle.
|
|
5
|
+
function firstTag(block, name) {
|
|
6
|
+
const m = block.match(new RegExp(`<${name}\\b[^>]*>([\\s\\S]*?)</${name}>`, "i"));
|
|
7
|
+
return m ? m[1].trim() : null;
|
|
8
|
+
}
|
|
9
|
+
function linkHref(block) {
|
|
10
|
+
// Prefer an alternate/self <link href="…">; fall back to a text <link>…</link>.
|
|
11
|
+
const href = block.match(/<link\b[^>]*\bhref="([^"]+)"/i);
|
|
12
|
+
if (href)
|
|
13
|
+
return href[1];
|
|
14
|
+
return firstTag(block, "link");
|
|
15
|
+
}
|
|
16
|
+
function authorName(block) {
|
|
17
|
+
const author = block.match(/<author\b[^>]*>([\s\S]*?)<\/author>/i);
|
|
18
|
+
if (!author)
|
|
19
|
+
return null;
|
|
20
|
+
return firstTag(author[1], "name");
|
|
21
|
+
}
|
|
22
|
+
export function unescapeXml(value) {
|
|
23
|
+
if (value == null)
|
|
24
|
+
return null;
|
|
25
|
+
return value
|
|
26
|
+
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1")
|
|
27
|
+
.replace(/</g, "<")
|
|
28
|
+
.replace(/>/g, ">")
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/'/g, "'")
|
|
31
|
+
.replace(/'/g, "'")
|
|
32
|
+
.replace(/&/g, "&")
|
|
33
|
+
.replace(/<[^>]+>/g, "") // strip any inline markup left in titles/summaries
|
|
34
|
+
.trim();
|
|
35
|
+
}
|
|
36
|
+
export function parseAtomEntries(xml) {
|
|
37
|
+
const blocks = xml.split(/<entry\b/i).slice(1);
|
|
38
|
+
return blocks.map((raw) => {
|
|
39
|
+
const block = raw.split(/<\/entry>/i)[0];
|
|
40
|
+
return {
|
|
41
|
+
id: firstTag(block, "id"),
|
|
42
|
+
title: unescapeXml(firstTag(block, "title")),
|
|
43
|
+
link: linkHref(block),
|
|
44
|
+
published: firstTag(block, "published") ?? firstTag(block, "updated"),
|
|
45
|
+
author: authorName(block),
|
|
46
|
+
summary: unescapeXml(firstTag(block, "summary")),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AdapterOptions, AdapterResult, YtDlp } from "./types.js";
|
|
2
|
+
export declare class MissingBinaryError extends Error {
|
|
3
|
+
constructor(message?: string);
|
|
4
|
+
}
|
|
5
|
+
export declare const defaultYtDlp: YtDlp;
|
|
6
|
+
export declare function parseVtt(vtt: string): string;
|
|
7
|
+
export declare function fetchYouTube(query: string, opts?: AdapterOptions): Promise<AdapterResult>;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// YouTube adapter — keyless, but via the yt-dlp binary rather than an HTTP API
|
|
2
|
+
// (upstream last30days does the same: "no API keys needed — just have yt-dlp
|
|
3
|
+
// installed"). yt-dlp is a *dependency*, not a secret, so YouTube stays a
|
|
4
|
+
// free-tier source (D-004). When yt-dlp is absent the source degrades honestly
|
|
5
|
+
// and the run continues — it never crashes (R-003 done_when).
|
|
6
|
+
//
|
|
7
|
+
// V0 covers recency-scoped search with view-count engagement. Transcript
|
|
8
|
+
// extraction (a bounded per-video enrichment) is a follow-on — see OQ-5 — kept
|
|
9
|
+
// out of V0 because it needs on-disk subtitle handling that would ship untested.
|
|
10
|
+
//
|
|
11
|
+
// The yt-dlp boundary is injected (types.ts YtDlp) so the adapter's recency and
|
|
12
|
+
// status logic is unit-tested with a fake and no binary runs in the suite.
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { promisify } from "node:util";
|
|
18
|
+
import { resolveOptions, settleStatus, withinWindow } from "./types.js";
|
|
19
|
+
const execFileAsync = promisify(execFile);
|
|
20
|
+
// How many top videos to enrich with a transcript per run — bounded so a run
|
|
21
|
+
// never fans out an unbounded number of subtitle fetches.
|
|
22
|
+
const TRANSCRIPT_LIMIT = 3;
|
|
23
|
+
const TRANSCRIPT_MAX_CHARS = 500;
|
|
24
|
+
// Hard ceiling on each yt-dlp process. A shell-out has no fetch signal to
|
|
25
|
+
// respect, so without this a hung yt-dlp stalled the whole gather for minutes
|
|
26
|
+
// (#774). Generous — a ytsearch fans out several upstream requests — but a
|
|
27
|
+
// bound: the child is killed and the adapter reports `excluded` like any other
|
|
28
|
+
// failure.
|
|
29
|
+
const YTDLP_TIMEOUT_MS = 120_000;
|
|
30
|
+
// Thrown by a YtDlp implementation when the binary is not installed, so the
|
|
31
|
+
// adapter can distinguish "not set up" (degrade + how-to) from "ran and failed".
|
|
32
|
+
export class MissingBinaryError extends Error {
|
|
33
|
+
constructor(message = "yt-dlp is not installed") {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "MissingBinaryError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Default implementation: shells out to yt-dlp and streams one JSON object per
|
|
39
|
+
// result. `ytsearchN:` is yt-dlp's search selector; --dump-json emits full
|
|
40
|
+
// metadata (upload_date, view_count) per video.
|
|
41
|
+
export const defaultYtDlp = {
|
|
42
|
+
async search(query, n) {
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await execFileAsync("yt-dlp", ["--dump-json", "--no-warnings", `ytsearch${n}:${query}`], { maxBuffer: 32 * 1024 * 1024, timeout: YTDLP_TIMEOUT_MS });
|
|
45
|
+
return stdout
|
|
46
|
+
.split("\n")
|
|
47
|
+
.map((line) => line.trim())
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.map((line) => {
|
|
50
|
+
try {
|
|
51
|
+
const v = JSON.parse(line);
|
|
52
|
+
if (!v.id || !v.title)
|
|
53
|
+
return null;
|
|
54
|
+
return {
|
|
55
|
+
id: v.id,
|
|
56
|
+
title: v.title,
|
|
57
|
+
url: v.webpage_url || v.url || `https://www.youtube.com/watch?v=${v.id}`,
|
|
58
|
+
uploader: v.uploader ?? v.channel ?? null,
|
|
59
|
+
uploadDate: v.upload_date ?? "",
|
|
60
|
+
viewCount: typeof v.view_count === "number" ? v.view_count : null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.filter((v) => v !== null);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const code = err.code;
|
|
71
|
+
if (code === "ENOENT")
|
|
72
|
+
throw new MissingBinaryError();
|
|
73
|
+
if (err.killed) {
|
|
74
|
+
throw new Error(`yt-dlp did not finish within ${YTDLP_TIMEOUT_MS / 1000}s (process killed)`);
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
// Best-effort auto-subtitle fetch. yt-dlp writes subtitles to disk, so we use
|
|
80
|
+
// an isolated temp dir, read the VTT back, and always clean up. Any failure
|
|
81
|
+
// (no captions, network, missing binary) yields null — a transcript is
|
|
82
|
+
// enrichment, never a reason to fail the item.
|
|
83
|
+
async transcript(videoId) {
|
|
84
|
+
let dir;
|
|
85
|
+
try {
|
|
86
|
+
dir = await mkdtemp(path.join(tmpdir(), "foo-yt-"));
|
|
87
|
+
const url = `https://www.youtube.com/watch?v=${videoId}`;
|
|
88
|
+
await execFileAsync("yt-dlp", [
|
|
89
|
+
"--skip-download",
|
|
90
|
+
"--write-auto-subs",
|
|
91
|
+
"--sub-lang",
|
|
92
|
+
"en",
|
|
93
|
+
"--sub-format",
|
|
94
|
+
"vtt",
|
|
95
|
+
"--no-warnings",
|
|
96
|
+
"-o",
|
|
97
|
+
path.join(dir, "%(id)s.%(ext)s"),
|
|
98
|
+
url,
|
|
99
|
+
], { maxBuffer: 32 * 1024 * 1024, timeout: YTDLP_TIMEOUT_MS });
|
|
100
|
+
const files = await readdir(dir);
|
|
101
|
+
const vttFile = files.find((f) => f.endsWith(".vtt"));
|
|
102
|
+
if (!vttFile)
|
|
103
|
+
return null;
|
|
104
|
+
const text = parseVtt(await readFile(path.join(dir, vttFile), "utf8"));
|
|
105
|
+
return text || null;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
if (dir)
|
|
112
|
+
await rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
// Reduce a WebVTT caption file to plain running text: drop the header, cue
|
|
117
|
+
// indices, timestamp lines, and inline tags, and collapse the duplicate lines
|
|
118
|
+
// that auto-generated rolling captions produce.
|
|
119
|
+
export function parseVtt(vtt) {
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const raw of vtt.split(/\r?\n/)) {
|
|
122
|
+
const line = raw.trim();
|
|
123
|
+
if (!line || line === "WEBVTT")
|
|
124
|
+
continue;
|
|
125
|
+
if (line.startsWith("Kind:") || line.startsWith("Language:"))
|
|
126
|
+
continue;
|
|
127
|
+
if (line.includes("-->"))
|
|
128
|
+
continue; // timestamp cue
|
|
129
|
+
if (/^\d+$/.test(line))
|
|
130
|
+
continue; // cue index
|
|
131
|
+
const text = line
|
|
132
|
+
.replace(/<[^>]+>/g, "")
|
|
133
|
+
.replace(/ /g, " ")
|
|
134
|
+
.trim();
|
|
135
|
+
if (!text || out[out.length - 1] === text)
|
|
136
|
+
continue; // dedupe rolling captions
|
|
137
|
+
out.push(text);
|
|
138
|
+
}
|
|
139
|
+
return out.join(" ");
|
|
140
|
+
}
|
|
141
|
+
// yt-dlp's upload_date is "YYYYMMDD"; normalize to an ISO instant so the shared
|
|
142
|
+
// recency filter can parse it. Pass through anything already ISO-ish.
|
|
143
|
+
function normalizeDate(raw) {
|
|
144
|
+
const m = /^(\d{4})(\d{2})(\d{2})$/.exec(raw);
|
|
145
|
+
if (m)
|
|
146
|
+
return `${m[1]}-${m[2]}-${m[3]}T00:00:00Z`;
|
|
147
|
+
return raw;
|
|
148
|
+
}
|
|
149
|
+
function toItem(v) {
|
|
150
|
+
if (!v.uploadDate)
|
|
151
|
+
return null; // no date → cannot place in the window
|
|
152
|
+
return {
|
|
153
|
+
source: "youtube",
|
|
154
|
+
id: `youtube:${v.id}`,
|
|
155
|
+
title: v.title,
|
|
156
|
+
url: v.url,
|
|
157
|
+
author: v.uploader,
|
|
158
|
+
createdAt: normalizeDate(v.uploadDate),
|
|
159
|
+
score: v.viewCount,
|
|
160
|
+
snippet: null,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export async function fetchYouTube(query, opts = {}) {
|
|
164
|
+
const { days, limit, now } = resolveOptions(opts);
|
|
165
|
+
const ytdlp = opts.ytdlp ?? defaultYtDlp;
|
|
166
|
+
try {
|
|
167
|
+
const videos = await ytdlp.search(query, limit);
|
|
168
|
+
const items = videos
|
|
169
|
+
.map(toItem)
|
|
170
|
+
.filter((it) => it !== null)
|
|
171
|
+
.filter((it) => withinWindow(it.createdAt, now, days))
|
|
172
|
+
.slice(0, limit);
|
|
173
|
+
if (items.length === 0) {
|
|
174
|
+
return {
|
|
175
|
+
source: "youtube",
|
|
176
|
+
status: "degraded",
|
|
177
|
+
reason: `yt-dlp ran but found no videos within ${days}d.`,
|
|
178
|
+
items,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// Best-effort transcript enrichment for the top few videos (bounded).
|
|
182
|
+
let transcribed = 0;
|
|
183
|
+
if (ytdlp.transcript) {
|
|
184
|
+
const fetchTranscript = ytdlp.transcript.bind(ytdlp);
|
|
185
|
+
await Promise.all(items.slice(0, TRANSCRIPT_LIMIT).map(async (it) => {
|
|
186
|
+
const text = await fetchTranscript(it.id.replace(/^youtube:/, ""));
|
|
187
|
+
if (text) {
|
|
188
|
+
it.snippet = text.slice(0, TRANSCRIPT_MAX_CHARS);
|
|
189
|
+
transcribed += 1;
|
|
190
|
+
}
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
source: "youtube",
|
|
195
|
+
status: settleStatus("available", items.length),
|
|
196
|
+
reason: `yt-dlp returned ${items.length} video(s) within ${days}d` +
|
|
197
|
+
(transcribed > 0 ? `, ${transcribed} with transcript.` : "."),
|
|
198
|
+
items,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (err instanceof MissingBinaryError) {
|
|
203
|
+
return {
|
|
204
|
+
source: "youtube",
|
|
205
|
+
status: "excluded",
|
|
206
|
+
reason: "yt-dlp is not installed; install it (e.g. brew install yt-dlp) to enable YouTube.",
|
|
207
|
+
items: [],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
source: "youtube",
|
|
212
|
+
status: "excluded",
|
|
213
|
+
reason: `yt-dlp failed: ${err.message}`,
|
|
214
|
+
items: [],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|